diff --git a/android/src/main/kotlin/ch/dissem/yaep/android/MainActivity.kt b/android/src/main/kotlin/ch/dissem/yaep/android/MainActivity.kt index 1510fc0..9e752d0 100644 --- a/android/src/main/kotlin/ch/dissem/yaep/android/MainActivity.kt +++ b/android/src/main/kotlin/ch/dissem/yaep/android/MainActivity.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import ch.dissem.yaep.domain.generateGame import ch.dissem.yaep.ui.common.App +import ch.dissem.yaep.ui.common.focus.FocusFollowingSelectionManager import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import yaep.commonui.generated.resources.action_restart @@ -63,8 +64,8 @@ class MainActivity : ComponentActivity() { ) { insets -> App( modifier = Modifier.padding(insets), + rootSelectionManager = FocusFollowingSelectionManager, spacing = 4.dp, - selectDirectly = false, game = game, onNewGame = { game = generateGame() }, resetCluesBeacon = resetCluesBeacon @@ -83,7 +84,7 @@ fun AppAndroidPreview() { App( game = game, spacing = 4.dp, - selectDirectly = false, + rootSelectionManager = FocusFollowingSelectionManager, onNewGame = { game = generateGame() }, resetCluesBeacon = resetCluesBeacon ) 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 af40dcc..713f1d9 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,6 +1,5 @@ package ch.dissem.yaep.ui.common -import SelectionManager import androidx.compose.foundation.layout.Box import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -11,15 +10,15 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.input.key.Key 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 ch.dissem.yaep.domain.Game -import focus +import ch.dissem.yaep.ui.common.focus.SelectionManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlin.coroutines.CoroutineContext import kotlin.time.ExperimentalTime @@ -27,8 +26,7 @@ import kotlin.time.ExperimentalTime @OptIn(ExperimentalTime::class) fun App( modifier: Modifier = Modifier, - selectionManager: SelectionManager, - selectDirectly: Boolean, + rootSelectionManager: SelectionManager<*>, spacing: Dp, game: Game, onNewGame: () -> Unit, @@ -57,12 +55,9 @@ fun App( Box(modifier = modifier) { AdaptiveGameLayout( modifier = Modifier.blurOnFinished(isSolved), - grid = { - val focusable = remember { selectionManager.add() } + selectionManager = rootSelectionManager, + grid = { selectionManager -> PuzzleGrid( - modifier = Modifier - .focus(focusable), - selectDirectly = selectDirectly, selectionManager = selectionManager, grid = game.grid, spacing = spacing, @@ -72,12 +67,11 @@ fun App( } ) }, - horizontalClues = { - val focusable = remember { selectionManager.add() } + horizontalClues = { selectionManager -> for (clue in horizontalClues) { HorizontalClue( modifier = Modifier - .focus(focusable) + .focus(remember { selectionManager.add() }) .forClue(clue, spacing), spacing = spacing, clue = clue.clue, @@ -85,23 +79,24 @@ fun App( ) } }, - verticalClues = { - val focusable = remember { selectionManager.add() } - for (clue in verticalClues) { - VerticalClue( - modifier = Modifier - .focus(focusable) - .forClue(clue, spacing), - spacing = spacing, - clue = clue.clue, - isClueViolated = clue.isViolated - ) + verticalClues = {selectionManager -> + if (verticalClues.isNotEmpty()) { + for (clue in verticalClues) { + VerticalClue( + modifier = Modifier + .focus(remember { selectionManager.add() }) + .forClue(clue, spacing), + spacing = spacing, + clue = clue.clue, + isClueViolated = clue.isViolated + ) + } } }, time = { Text( time, - fontSize = TextUnit(4f, TextUnitType.Companion.Em), + fontSize = TextUnit(4f, TextUnitType.Em), textAlign = TextAlign.End ) }, diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/adaptive game layout.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/adaptive game layout.kt index f446f55..3f7dd8e 100644 --- a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/adaptive game layout.kt +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/adaptive game layout.kt @@ -1,8 +1,11 @@ package ch.dissem.yaep.ui.common +import androidx.compose.foundation.layout.Box import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.Key import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.Placeable @@ -14,6 +17,11 @@ import androidx.compose.ui.unit.dp import ch.dissem.yaep.ui.common.AspectRatio.LANDSCAPE import ch.dissem.yaep.ui.common.AspectRatio.PORTRAIT import ch.dissem.yaep.ui.common.AspectRatio.SQUARISH +import ch.dissem.yaep.ui.common.focus.FocusFollowingFocusable +import ch.dissem.yaep.ui.common.focus.GridSelectionManager +import ch.dissem.yaep.ui.common.focus.LinearSelectionManager +import ch.dissem.yaep.ui.common.focus.SelectionManager +import kotlin.math.ceil import kotlin.math.max import kotlin.math.min @@ -39,37 +47,74 @@ private enum class AspectRatio { @Composable fun AdaptiveGameLayout( modifier: Modifier = Modifier, - grid: @Composable () -> Unit, - horizontalClues: @Composable () -> Unit, - verticalClues: @Composable () -> Unit, + selectionManager: SelectionManager<*>, + grid: @Composable (GridSelectionManager) -> Unit, + horizontalClues: @Composable (SelectionManager<*>) -> Unit, + verticalClues: @Composable (SelectionManager<*>) -> Unit, time: @Composable () -> Unit, divider: @Composable () -> Unit = { HorizontalDivider() }, spacing: Dp = 8.dp ) { + val gridFocusable = selectionManager.add() + val gridSelectionManager = gridFocusable.create(GridSelectionManager()) + + val horizontalCluesFocusable = selectionManager.add() + val horizontalCluesSelectionManager = horizontalCluesFocusable.create( + LinearSelectionManager(Key.DirectionRight, Key.DirectionLeft) + ) + + val verticalCluesFocusable = selectionManager.add() + val verticalCluesSelectionManager = verticalCluesFocusable.create( + LinearSelectionManager(Key.DirectionRight, Key.DirectionLeft) + ) Layout( - contents = listOf(grid, horizontalClues, verticalClues, time, divider, divider), + contents = listOf( + { + Box(modifier = Modifier.focus(gridFocusable)) + LaunchedEffect(Unit) { + if (gridFocusable is FocusFollowingFocusable) { + gridFocusable.focusRequester.requestFocus() + } + } + }, + { Box(modifier = Modifier.focus(horizontalCluesFocusable)) }, + { Box(modifier = Modifier.focus(verticalCluesFocusable)) }, + { grid(gridSelectionManager) }, + { horizontalClues(horizontalCluesSelectionManager) }, + { verticalClues(verticalCluesSelectionManager) }, + time, + divider, + divider + ), modifier = modifier ) { measurables, constraints -> layout(width = constraints.maxWidth, height = constraints.maxHeight) { val aspectRatio = AspectRatio.from(constraints) - val gridMeasurable = measurables[0][0] - val horizontalCluesMeasurables = measurables[1] - val verticalCluesMeasurables = measurables[2] - val timeMeasurable = measurables[3][0] - val dividerMeasurable = measurables[4][0] + val gridBoxMeasurable = measurables[0][0] + val horizontalCluesBoxMeasurable = measurables[1][0] + val verticalCluesBoxMeasurable = measurables[2][0] + + val gridMeasurable = measurables[3][0] + val horizontalCluesMeasurables = measurables[4] + val verticalCluesMeasurables = measurables[5] + val timeMeasurable = measurables[6][0] + val dividerMeasurable = measurables[7][0] val spacingPx = spacing.roundToPx() when (aspectRatio) { PORTRAIT -> { - val divider2Measurable = measurables[5][0] + val divider2Measurable = measurables[8][0] portrait( constraints, spacingPx, gridMeasurable, + gridBoxMeasurable, horizontalCluesMeasurables, + horizontalCluesBoxMeasurable, verticalCluesMeasurables, + verticalCluesBoxMeasurable, timeMeasurable, dividerMeasurable, divider2Measurable @@ -81,8 +126,11 @@ fun AdaptiveGameLayout( constraints, spacingPx, gridMeasurable, + gridBoxMeasurable, horizontalCluesMeasurables, + horizontalCluesBoxMeasurable, verticalCluesMeasurables, + verticalCluesBoxMeasurable, timeMeasurable ) } @@ -92,8 +140,11 @@ fun AdaptiveGameLayout( constraints, spacingPx, gridMeasurable, + gridBoxMeasurable, horizontalCluesMeasurables, + horizontalCluesBoxMeasurable, verticalCluesMeasurables, + verticalCluesBoxMeasurable, timeMeasurable, dividerMeasurable ) @@ -107,8 +158,11 @@ private fun Placeable.PlacementScope.portrait( constraints: Constraints, spacingPx: Int, gridMeasurable: Measurable, + gridBoxMeasurable: Measurable, horizontalCluesMeasurables: List, + horizontalCluesBoxMeasurable: Measurable, verticalCluesMeasurables: List, + verticalCluesBoxMeasurable: Measurable, timeMeasurable: Measurable, divider1Measurable: Measurable, divider2Measurable: Measurable @@ -129,6 +183,7 @@ private fun Placeable.PlacementScope.portrait( val dividerConstraints = fixedWidth(gridSize) val gridPlaceable = gridMeasurable.measure(gridConstraints) + val gridBoxPlaceable = gridBoxMeasurable.measure(gridConstraints) val horizontalCluesPlaceables = horizontalCluesMeasurables.map { it.measure(horizontalCluesConstraints) } @@ -139,29 +194,53 @@ private fun Placeable.PlacementScope.portrait( val divider1Placeable = divider1Measurable.measure(dividerConstraints) val divider2Placeable = divider2Measurable.measure(dividerConstraints) + val horizontalCluesBoxPlaceable = horizontalCluesBoxMeasurable + .measure( + cluesBoxConstraints( + width = gridSize, + itemConstraints = horizontalCluesConstraints, + itemCount = horizontalCluesMeasurables.size + ) + ) + val verticalCluesBoxPlaceable = verticalCluesBoxMeasurable + .measure( + cluesBoxConstraints( + width = gridSize, + itemConstraints = verticalCluesConstraints, + itemCount = verticalCluesMeasurables.size + ) + ) + // Position the grid gridPlaceable.place(0, 0) + gridBoxPlaceable.place(0, 0) divider1Placeable.place(0, gridSize + spacingPx) // Position the horizontal clues + val horizontalCluesOffsetY = gridSize + 2 * spacingPx + var offsetY = placeClues( placeables = horizontalCluesPlaceables, offsetX = 0, - offsetY = gridSize + 2 * spacingPx, + offsetY = horizontalCluesOffsetY, maxWidth = gridSize ) + horizontalCluesBoxPlaceable.place(0, horizontalCluesOffsetY) + verticalCluesBoxPlaceable.place(0, horizontalCluesOffsetY) // Add divider in between divider2Placeable.place(0, offsetY + spacingPx) // Position the vertical clues + val verticalCluesOffsetY = offsetY + spacingPx + divider2Placeable.height offsetY = placeClues( placeables = verticalCluesPlaceables, offsetX = 0, - offsetY = offsetY + spacingPx + divider2Placeable.height, + offsetY = verticalCluesOffsetY, maxWidth = gridSize ) + verticalCluesBoxPlaceable.place(0, verticalCluesOffsetY) // Position the time val remainingSpace = constraints.maxHeight - offsetY @@ -190,8 +269,11 @@ private fun Placeable.PlacementScope.squarish( constraints: Constraints, spacingPx: Int, gridMeasurable: Measurable, + gridBoxMeasurable: Measurable, horizontalCluesMeasurables: List, + horizontalCluesBoxMeasurable: Measurable, verticalCluesMeasurables: List, + verticalCluesBoxMeasurable: Measurable, timeMeasurable: Measurable ) { val gridSize = (7 * min(constraints.maxWidth, constraints.maxHeight)) / 10 @@ -210,32 +292,55 @@ private fun Placeable.PlacementScope.squarish( val timeConstraints = Constraints() val gridPlaceable = gridMeasurable.measure(gridConstraints) + val gridBoxPlaceable = gridBoxMeasurable.measure(gridConstraints) val horizontalCluesPlaceables = horizontalCluesMeasurables.map { it.measure(horizontalCluesConstraints) } + val horizontalCluesBoxPlaceable = horizontalCluesBoxMeasurable + .measure( + cluesBoxConstraints( + width = rightBarWidth, + itemConstraints = horizontalCluesConstraints, + itemCount = horizontalCluesMeasurables.size + ) + ) + val verticalCluesPlaceables = verticalCluesMeasurables.map { it.measure(verticalCluesConstraints) } + val verticalCluesBoxPlaceable = verticalCluesBoxMeasurable + .measure( + cluesBoxConstraints( + width = rightBarWidth, + itemConstraints = verticalCluesConstraints, + itemCount = verticalCluesMeasurables.size + ) + ) val timePlaceable = timeMeasurable.measure(timeConstraints) // Position the grid gridPlaceable.place(0, 0) + gridBoxPlaceable.place(0, 0) // Position the horizontal clues + val horizontalCluesOffsetX = gridSize + 2 * spacingPx placeClues( placeables = horizontalCluesPlaceables, - offsetX = gridSize + 2 * spacingPx, + offsetX = horizontalCluesOffsetX, offsetY = 0, maxWidth = rightBarWidth ) + horizontalCluesBoxPlaceable.place(horizontalCluesOffsetX, 0) // Position the vertical clues + val verticalCluesOffsetY = gridSize + 2 * spacingPx placeClues( placeables = verticalCluesPlaceables, offsetX = 0, - offsetY = gridSize + 2 * spacingPx, + offsetY = verticalCluesOffsetY, maxWidth = gridSize ) + verticalCluesBoxPlaceable.place(0, verticalCluesOffsetY) // Position the time timePlaceable.place( @@ -248,8 +353,11 @@ private fun Placeable.PlacementScope.landscape( constraints: Constraints, spacingPx: Int, gridMeasurable: Measurable, + gridBoxMeasurable: Measurable, horizontalCluesMeasurables: List, + horizontalCluesBoxMeasurable: Measurable, verticalCluesMeasurables: List, + verticalCluesBoxMeasurable: Measurable, timeMeasurable: Measurable, dividerMeasurable: Measurable ) { @@ -271,36 +379,61 @@ private fun Placeable.PlacementScope.landscape( val dividerConstraints = fixedWidth(rightBarWidth - 2 * spacingPx) val gridPlaceable = gridMeasurable.measure(gridConstraints) + val gridBoxPlaceable = gridBoxMeasurable.measure(gridConstraints) val horizontalCluesPlaceables = horizontalCluesMeasurables.map { it.measure(horizontalCluesConstraints) } + val horizontalCluesBoxPlaceable = horizontalCluesBoxMeasurable + .measure( + cluesBoxConstraints( + width = rightBarWidth, + itemConstraints = horizontalCluesConstraints, + itemCount = horizontalCluesMeasurables.size + ) + ) + val verticalCluesPlaceables = verticalCluesMeasurables.map { it.measure(verticalCluesConstraints) } + val verticalCluesBoxPlaceable = verticalCluesBoxMeasurable + .measure( + cluesBoxConstraints( + width = rightBarWidth, + itemConstraints = verticalCluesConstraints, + itemCount = verticalCluesMeasurables.size + ) + ) + val timePlaceable = timeMeasurable.measure(timeConstraints) val dividerPlaceable = dividerMeasurable.measure(dividerConstraints) // Position the grid gridPlaceable.place(0, 0) + gridBoxPlaceable.place(0, 0) - // Position the horizontal clues + // Position the horizontal clues4 + val horizontalCluesOffsetX = gridSize + 2 * spacingPx val offsetY = placeClues( placeables = horizontalCluesPlaceables, - offsetX = gridSize + 2 * spacingPx, + offsetX = horizontalCluesOffsetX, offsetY = 0, maxWidth = rightBarWidth ) + horizontalCluesBoxPlaceable.place(horizontalCluesOffsetX, 0) // Add divider in between dividerPlaceable.place(gridSize + 3 * spacingPx, offsetY + spacingPx) // Position the vertical clues + val verticalCluesOffsetX = gridSize + 2 * spacingPx + val verticalCluesOffsetY = offsetY + spacingPx + dividerPlaceable.height placeClues( placeables = verticalCluesPlaceables, - offsetX = gridSize + 2 * spacingPx, - offsetY = offsetY + spacingPx + dividerPlaceable.height, + offsetX = verticalCluesOffsetX, + offsetY = verticalCluesOffsetY, maxWidth = rightBarWidth ) + verticalCluesBoxPlaceable.place(verticalCluesOffsetX, verticalCluesOffsetY) // Position the time timePlaceable.place( @@ -309,6 +442,15 @@ private fun Placeable.PlacementScope.landscape( ) } +private fun cluesBoxConstraints( + width: Int, + itemConstraints: Constraints, + itemCount: Int +): Constraints = fixed( + width, + itemConstraints.maxHeight * ceil(itemCount.toFloat() / (width / itemConstraints.maxWidth)).toInt() +) + private fun Placeable.PlacementScope.placeClues( placeables: List, offsetX: Int, diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/FocusFollowingFocusable.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/FocusFollowingFocusable.kt new file mode 100644 index 0000000..dedc086 --- /dev/null +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/FocusFollowingFocusable.kt @@ -0,0 +1,18 @@ +package ch.dissem.yaep.ui.common.focus + +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.FocusState + +class FocusFollowingFocusable(manager: FocusFollowingSelectionManager) : + Focusable(manager) { + + val focusRequester = FocusRequester() + + fun setFocus(state: FocusState) { + if (state.hasFocus) { + manager.focused = this + } else if (manager.focused === this) { + manager.focused = null + } + } +} \ No newline at end of file diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/FocusFollowingSelectionManager.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/FocusFollowingSelectionManager.kt new file mode 100644 index 0000000..11bab76 --- /dev/null +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/FocusFollowingSelectionManager.kt @@ -0,0 +1,17 @@ +package ch.dissem.yaep.ui.common.focus + +import androidx.compose.ui.input.key.KeyEvent + +object FocusFollowingSelectionManager : SelectionManager() { + init { + isActive = true + } + + override fun add(): FocusFollowingFocusable { + return FocusFollowingFocusable(this) + } + + // Key events are ignored, the default focus mechanisms are used + override fun onKeyEvent(event: KeyEvent): Boolean = false + +} \ No newline at end of file diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/Focusable.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/Focusable.kt new file mode 100644 index 0000000..d22f1a2 --- /dev/null +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/Focusable.kt @@ -0,0 +1,21 @@ +package ch.dissem.yaep.ui.common.focus + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +abstract class Focusable>(protected val manager: SelectionManager) { + + val hasFocus: Flow = + combine(manager.isActiveFlow, manager.focusedFlow) { isActive, focused -> + isActive && focused == this + } + + var child: SelectionManager<*>? = null + private set + + fun > create(selectionManager: M): M = selectionManager.apply { + isActive = manager.isActive && manager.focused == this@Focusable + this@Focusable.child = this + } + +} \ No newline at end of file diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/GridFocusable.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/GridFocusable.kt new file mode 100644 index 0000000..b359b41 --- /dev/null +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/GridFocusable.kt @@ -0,0 +1,3 @@ +package ch.dissem.yaep.ui.common.focus + +class GridFocusable(manager: GridSelectionManager) : Focusable(manager) \ No newline at end of file diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/GridSelectionManager.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/GridSelectionManager.kt new file mode 100644 index 0000000..9139c37 --- /dev/null +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/GridSelectionManager.kt @@ -0,0 +1,65 @@ +package ch.dissem.yaep.ui.common.focus + +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.type + +class GridSelectionManager : SelectionManager() { + + val grid = mutableListOf>() + + var row = 0 + var col = 0 + + fun addRow(): GridSelectionManager { + grid.add(mutableListOf()) + return this + } + + override fun add(): GridFocusable { + val new = GridFocusable(this) + grid.last().add(new) + return new + } + + override fun onKeyEvent(event: KeyEvent): Boolean { + if (event.type != KeyEventType.KeyUp) return false + + when (event.key) { + Key.DirectionDown -> { + row++ + if (row >= grid.size) { + row = 0 + } + } + + Key.DirectionUp -> { + row-- + if (row < 0) { + row = grid.size - 1 + } + } + + Key.DirectionRight -> { + col++ + if (col >= grid[row].size) { + col = 0 + } + } + + Key.DirectionLeft -> { + col-- + if (col < 0) { + col = grid[row].size - 1 + } + } + + else -> return focused?.child?.onKeyEvent(event) == true + } + + focused = grid[row][col] + return true + } +} \ No newline at end of file diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/LinearFocusable.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/LinearFocusable.kt new file mode 100644 index 0000000..f22a0b3 --- /dev/null +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/LinearFocusable.kt @@ -0,0 +1,17 @@ +package ch.dissem.yaep.ui.common.focus + +class LinearFocusable(manager: LinearSelectionManager) : Focusable(manager) { + + var previous: LinearFocusable = this + private set + + private var _next: LinearFocusable = this + var next: LinearFocusable + get() = _next + set(value) { + previous = value.previous + previous._next = this + value.previous = this + _next = value + } +} \ No newline at end of file diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/LinearSelectionManager.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/LinearSelectionManager.kt new file mode 100644 index 0000000..4aa1573 --- /dev/null +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/LinearSelectionManager.kt @@ -0,0 +1,49 @@ +package ch.dissem.yaep.ui.common.focus + +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEvent +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.isShiftPressed +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.type + +class LinearSelectionManager( + val keyNext: Key, + val keyPrevious: Key? = null +) : SelectionManager() { + + private fun focusNext() { + focused = focused?.next + } + + private fun focusPrevious() { + focused = focused?.previous + } + + override fun add(): LinearFocusable { + val new = LinearFocusable(this) + if (focused != null) { + new.next = focused!! + } else { + focused = new + } + return new + } + + override fun onKeyEvent(event: KeyEvent): Boolean { + if (event.type != KeyEventType.KeyUp) return false + + if (event.key == keyNext) { + if (keyPrevious == null && event.isShiftPressed) { + focusPrevious() + } else { + focusNext() + } + } else if (event.key == keyPrevious) { + focusPrevious() + } else { + return focused?.child?.onKeyEvent(event) == true + } + return true + } +} \ No newline at end of file diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/SelectionManager.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/SelectionManager.kt new file mode 100644 index 0000000..e53f566 --- /dev/null +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/focus/SelectionManager.kt @@ -0,0 +1,29 @@ +package ch.dissem.yaep.ui.common.focus + +import androidx.compose.ui.input.key.KeyEvent +import kotlinx.coroutines.flow.MutableStateFlow + +abstract class SelectionManager> { + var isActiveFlow = MutableStateFlow(false) + var isActive: Boolean + get() = isActiveFlow.value + set(value) { + isActiveFlow.value = value + } + + var focusedFlow = MutableStateFlow(null) + + var focused: F? + get() = focusedFlow.value + set(value) { + val previous = focusedFlow.value + if (previous != value) { + previous?.child?.isActive = false + value?.child?.isActive = true + focusedFlow.value = value + } + } + + abstract fun add(): F + abstract fun onKeyEvent(event: KeyEvent): Boolean +} \ No newline at end of file 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 index e939d59..c133b52 100644 --- a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/grid.kt +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/grid.kt @@ -1,6 +1,5 @@ package ch.dissem.yaep.ui.common -import SelectionManager import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -13,7 +12,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue 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.GameCell @@ -21,13 +19,12 @@ import ch.dissem.yaep.domain.GameRow import ch.dissem.yaep.domain.Grid import ch.dissem.yaep.domain.Item import ch.dissem.yaep.domain.ItemClass -import focus +import ch.dissem.yaep.ui.common.focus.GridSelectionManager @Composable fun PuzzleGrid( modifier: Modifier = Modifier, - selectDirectly: Boolean, - selectionManager: SelectionManager, + selectionManager: GridSelectionManager, spacing: Dp = 8.dp, grid: Grid, onUpdate: () -> Unit @@ -40,8 +37,7 @@ fun PuzzleGrid( onSnapshot = { grid.snapshot() }, onUndo = { grid.undo() }, spacing = spacing, - selectDirectly = selectDirectly, - selectionManager = selectionManager + selectionManager = remember { selectionManager.addRow() } ) } } @@ -54,20 +50,15 @@ private fun PuzzleRow( onSnapshot: () -> Unit, onUndo: () -> Boolean, spacing: Dp, - selectDirectly: Boolean, - selectionManager: SelectionManager + selectionManager: GridSelectionManager ) { - val focusableRow = remember { selectionManager.add() } Row( modifier = Modifier .fillMaxWidth() .wrapContentHeight() ) { val allOptions = row.options - val columnSelectionManager = - remember { focusableRow.createChild(Key.DirectionRight, Key.DirectionLeft) } for (cell in row) { - val focusableItem = remember { columnSelectionManager.add() } var selection by remember(cell) { mutableStateOf(cell.selection) } val options = remember(cell) { allOptions.map { Toggleable(it, cell.options.contains(it)) } @@ -83,11 +74,10 @@ private fun PuzzleRow( } Selector( modifier = Modifier - .focus(focusableItem) + .focus(remember { selectionManager.add() }) .padding(spacing) .weight(1f), spacing, - selectDirectly = selectDirectly, options = options, onOptionRemoved = { onSnapshot() diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/names.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/names.kt index 7a40b68..f52fcc0 100644 --- a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/names.kt +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/names.kt @@ -13,16 +13,15 @@ import yaep.commonui.generated.resources.Res import yaep.commonui.generated.resources.ant import yaep.commonui.generated.resources.astronaut import yaep.commonui.generated.resources.banana -import yaep.commonui.generated.resources.bubble_tea import yaep.commonui.generated.resources.beverage import yaep.commonui.generated.resources.bicycle import yaep.commonui.generated.resources.british +import yaep.commonui.generated.resources.bubble_tea import yaep.commonui.generated.resources.bus import yaep.commonui.generated.resources.cake import yaep.commonui.generated.resources.canadian import yaep.commonui.generated.resources.cherries import yaep.commonui.generated.resources.chocolate -import yaep.commonui.generated.resources.water import yaep.commonui.generated.resources.coffee import yaep.commonui.generated.resources.cookie import yaep.commonui.generated.resources.cupcake @@ -41,6 +40,7 @@ import yaep.commonui.generated.resources.lemon import yaep.commonui.generated.resources.locomotive import yaep.commonui.generated.resources.lollipop import yaep.commonui.generated.resources.mango +import yaep.commonui.generated.resources.mate import yaep.commonui.generated.resources.milk import yaep.commonui.generated.resources.motor_scooter import yaep.commonui.generated.resources.norwegian @@ -63,8 +63,8 @@ import yaep.commonui.generated.resources.tea import yaep.commonui.generated.resources.teacher import yaep.commonui.generated.resources.tram_car import yaep.commonui.generated.resources.ukrainian +import yaep.commonui.generated.resources.water import yaep.commonui.generated.resources.watermelon -import yaep.commonui.generated.resources.mate import yaep.commonui.generated.resources.zebra val ItemClass<*>.localName: StringResource 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 deleted file mode 100644 index b83b0ac..0000000 --- a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/puzzle clues.kt +++ /dev/null @@ -1,136 +0,0 @@ -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.padding -import androidx.compose.material3.CardDefaults -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.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 - } - } -} - -internal 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, - clue: HorizontalClue, - isClueViolated: Boolean -) { - ClueCard( - modifier = modifier, - isClueViolated = isClueViolated - ) { - Row { - when (clue) { - is NeighbourClue<*, *> -> { - DrawItem(modifier = Modifier.weight(1f), clue.a) - Image( - modifier = Modifier.aspectRatio(1f).weight(1f), - painter = painterResource(Res.drawable.neighbour), - contentDescription = null - ) - DrawItem(modifier = Modifier.weight(1f), clue.b) - } - - is OrderClue<*, *> -> { - DrawItem(modifier = Modifier.weight(1f), clue.left) - Image( - modifier = Modifier.aspectRatio(1f).weight(1f), - painter = painterResource(Res.drawable.order), - contentDescription = null - ) - DrawItem(modifier = Modifier.weight(1f), clue.right) - } - - is TripletClue<*, *, *> -> { - DrawItem(modifier = Modifier.weight(1f), clue.a) - DrawItem(modifier = Modifier.weight(1f), clue.b) - DrawItem(modifier = Modifier.weight(1f), clue.c) - } - } - } - } -} - -@Composable -fun VerticalClue( - modifier: Modifier = Modifier, - clue: SameColumnClue<*, *>, - isClueViolated: Boolean = false -) { - ClueCard( - modifier = modifier.aspectRatio(0.5f), - isClueViolated = isClueViolated - ) { - Column { - DrawItem(modifier = Modifier.weight(1f), clue.a) - DrawItem(modifier = Modifier.weight(1f), clue.b) - } - } -} - -@Composable -fun ClueCard( - modifier: Modifier = Modifier, - 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 deleted file mode 100644 index 6bade7f..0000000 --- a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/puzzle grid.kt +++ /dev/null @@ -1,88 +0,0 @@ -package ch.dissem.yaep.ui.common - -import SelectionManager -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.input.key.Key -import androidx.compose.ui.unit.dp -import ch.dissem.yaep.domain.Grid -import focus - -@Composable -fun PuzzleGrid( - modifier: Modifier = Modifier, - selectionManager: SelectionManager, - grid: Grid, - onUpdate: () -> Unit -) { - Column(modifier = modifier) { - for (row in grid) { - val focusableRow = remember { selectionManager.add() } - Row( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - ) { - val allOptions = row.options - val columnSelectionManager = - remember { focusableRow.createChild(Key.DirectionRight, Key.DirectionLeft) } - for (item in row) { - val focusableItem = remember { columnSelectionManager.add() } - 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 - .focus(focusableItem) - .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/selector.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/selector.kt index 01a5c9e..b0f1566 100644 --- a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/selector.kt +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/selector.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.drawText import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import ch.dissem.yaep.domain.Item import ch.dissem.yaep.domain.ItemClass import ch.dissem.yaep.ui.common.theme.emojiFontFamily @@ -28,8 +29,7 @@ import kotlin.math.min @Composable fun > Selector( modifier: Modifier = Modifier, - spacing: Dp, - selectDirectly: Boolean, + spacing: Dp = 4.dp, options: List>>, onOptionRemoved: (Item) -> Unit, onOptionAdded: (Item) -> Unit, @@ -83,8 +83,8 @@ fun > Selector( @Composable fun > DrawItem( modifier: Modifier = Modifier, - spacing: Dp, - item: Item + item: Item, + spacing: Dp = 4.dp ) { OutlinedCard(modifier = modifier.aspectRatio(1f), shape = RoundedCornerShape(spacing)) { val emoji = item.symbol diff --git a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/theme/Color.kt b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/theme/Color.kt index 3e44904..93edae3 100644 --- a/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/theme/Color.kt +++ b/commonUI/src/commonMain/kotlin/ch/dissem/yaep/ui/common/theme/Color.kt @@ -1,4 +1,5 @@ package ch.dissem.yaep.ui.common.theme + import androidx.compose.ui.graphics.Color val primaryLight = Color(0xFF6D5E0F) 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 index d448521..b845d4d 100644 --- 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 @@ -1,128 +1,39 @@ +package ch.dissem.yaep.ui.common + import androidx.compose.foundation.border +import androidx.compose.foundation.focusable import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.KeyEvent -import androidx.compose.ui.input.key.KeyEventType -import androidx.compose.ui.input.key.isShiftPressed -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.type import androidx.compose.ui.unit.dp -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import kotlin.IllegalStateException - -class SelectionManager( - val keyNext: Key, - val keyPrevious: Key? = null -) { - var isActiveFlow = MutableStateFlow(false) - var isActive: Boolean - get() = isActiveFlow.value - set(value) { - isActiveFlow.value = value - } - - var focusedFlow = MutableStateFlow(null) - - var focused: Focusable? - get() = focusedFlow.value - set(value) { - val previous = focusedFlow.value - if (previous != value) { - previous?.child?.isActive = false - value?.child?.isActive = true - } - focusedFlow.value = value - } - - val last: Focusable - get() = focused?.previous ?: throw IllegalStateException("not initialized") - - val child: SelectionManager? - get() = focused?.child - - fun focusNext() { - focused = focused?.next - } - - fun focusPrevious() { - focused = focused?.previous - } - - fun add(): Focusable { - val new = Focusable(this) - if (focused != null) { - new.next = focused!! - } else { - focused = new - } - return new - } - - fun onKeyEvent(event: KeyEvent): Boolean { - if (event.type != KeyEventType.KeyUp) return false - - if (event.key == keyNext) { - if (keyPrevious == null && event.isShiftPressed) { - focusPrevious() - } else { - focusNext() - } - } else if (event.key == keyPrevious) { - focusPrevious() - } else { - return child?.onKeyEvent(event) == true - } - return true - } -} - -class Focusable( - private val manager: SelectionManager, -) { - val hasFocus: Flow = - combine(manager.isActiveFlow, manager.focusedFlow) { isActive, focused -> - isActive && focused == this - } - - var previous: Focusable = this - private set - - private var _next: Focusable = this - var next: Focusable - get() = _next - set(value) { - previous = value.previous - previous._next = this - value.previous = this - _next = value - } - - var child: SelectionManager? = null - private set - - fun createChild( - keyNext: Key, - keyPrevious: Key? = null - ): SelectionManager { - child = SelectionManager(keyNext, keyPrevious) - child!!.isActive = manager.isActive && manager.focused == this - return child!! - } -} +import ch.dissem.yaep.ui.common.focus.FocusFollowingFocusable +import ch.dissem.yaep.ui.common.focus.Focusable @Composable -fun Modifier.focus(holder: Focusable): Modifier = if (holder.hasFocus.collectAsState(false).value) { - border( - width = 2.dp, - color = MaterialTheme.colorScheme.primary, - shape = RectangleShape - ) -} else { - this +fun Modifier.focus(holder: Focusable<*>): Modifier { + var m = this + val hasFocus by holder.hasFocus.collectAsState(false) + if (hasFocus) { + m = m.border( + width = 2.dp, + color = MaterialTheme.colorScheme.primary, + shape = RectangleShape + ) + } + if (holder is FocusFollowingFocusable) { + m = m + .focusRequester(holder.focusRequester) + .onFocusEvent { state -> + holder.setFocus(state) + } + .onFocusChanged {} + .focusable() + } + return m } \ No newline at end of file diff --git a/desktop/src/main/kotlin/ch/dissem/yaep/ui/desktop/desktop window.kt b/desktop/src/main/kotlin/ch/dissem/yaep/ui/desktop/desktop window.kt index c5c9b2b..084bd3e 100644 --- a/desktop/src/main/kotlin/ch/dissem/yaep/ui/desktop/desktop window.kt +++ b/desktop/src/main/kotlin/ch/dissem/yaep/ui/desktop/desktop window.kt @@ -1,6 +1,5 @@ package ch.dissem.yaep.ui.desktop -import SelectionManager import androidx.compose.foundation.Image import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer @@ -17,12 +16,12 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.scale import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.unit.dp import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.WindowScope import androidx.compose.ui.window.WindowState +import ch.dissem.yaep.ui.common.focus.FocusFollowingSelectionManager import ch.dissem.yaep.ui.common.theme.AppTheme import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @@ -43,7 +42,7 @@ import yaep.desktop.generated.resources.Res as DRes @Composable fun WindowScope.DesktopWindow( useDarkMode: Boolean, - selectionManager: SelectionManager, + selectionManager: FocusFollowingSelectionManager, topBar: @Composable () -> Unit, content: @Composable (PaddingValues) -> Unit ) { diff --git a/desktop/src/main/kotlin/ch/dissem/yaep/ui/desktop/main.kt b/desktop/src/main/kotlin/ch/dissem/yaep/ui/desktop/main.kt index d5e355a..164a997 100644 --- a/desktop/src/main/kotlin/ch/dissem/yaep/ui/desktop/main.kt +++ b/desktop/src/main/kotlin/ch/dissem/yaep/ui/desktop/main.kt @@ -1,13 +1,11 @@ package ch.dissem.yaep.ui.desktop -import SelectionManager import androidx.compose.foundation.layout.padding 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.input.key.Key import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight @@ -20,6 +18,7 @@ import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState import ch.dissem.yaep.domain.generateGame import ch.dissem.yaep.ui.common.App +import ch.dissem.yaep.ui.common.focus.FocusFollowingSelectionManager import ch.dissem.yaep.ui.common.theme.emojiFontFamily import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @@ -49,12 +48,12 @@ fun main(): Unit = application { state = windowState, icon = painterResource(DRes.drawable.ic_launcher) ) { - val selectionManager = remember { SelectionManager(Key.Tab).apply { isActive = true } } + val rootSelectionManager = FocusFollowingSelectionManager var useDarkMode by remember { mutableStateOf(true) } var resetCluesBeacon by remember { mutableStateOf(Any()) } DesktopWindow( useDarkMode = useDarkMode, - selectionManager = selectionManager, + selectionManager = rootSelectionManager, topBar = { AppBar( useDarkMode = useDarkMode, @@ -71,9 +70,8 @@ fun main(): Unit = application { ) { App( modifier = Modifier.padding(it), - selectionManager = selectionManager, + rootSelectionManager = rootSelectionManager, spacing = 8.dp, - selectDirectly = true, game = game, onNewGame = { game = generateGame() }, resetCluesBeacon = resetCluesBeacon