Add keyboard control (WIP, broken)

This commit is contained in:
Christian Basler
2025-12-19 22:57:25 +01:00
parent e80ae7f722
commit 25d4da4582
20 changed files with 450 additions and 418 deletions

View File

@@ -20,6 +20,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ch.dissem.yaep.domain.generateGame import ch.dissem.yaep.domain.generateGame
import ch.dissem.yaep.ui.common.App 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.painterResource
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import yaep.commonui.generated.resources.action_restart import yaep.commonui.generated.resources.action_restart
@@ -63,8 +64,8 @@ class MainActivity : ComponentActivity() {
) { insets -> ) { insets ->
App( App(
modifier = Modifier.padding(insets), modifier = Modifier.padding(insets),
rootSelectionManager = FocusFollowingSelectionManager,
spacing = 4.dp, spacing = 4.dp,
selectDirectly = false,
game = game, game = game,
onNewGame = { game = generateGame() }, onNewGame = { game = generateGame() },
resetCluesBeacon = resetCluesBeacon resetCluesBeacon = resetCluesBeacon
@@ -83,7 +84,7 @@ fun AppAndroidPreview() {
App( App(
game = game, game = game,
spacing = 4.dp, spacing = 4.dp,
selectDirectly = false, rootSelectionManager = FocusFollowingSelectionManager,
onNewGame = { game = generateGame() }, onNewGame = { game = generateGame() },
resetCluesBeacon = resetCluesBeacon resetCluesBeacon = resetCluesBeacon
) )

View File

@@ -1,6 +1,5 @@
package ch.dissem.yaep.ui.common package ch.dissem.yaep.ui.common
import SelectionManager
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -11,15 +10,15 @@ 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 ch.dissem.yaep.ui.common.focus.SelectionManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
@@ -27,8 +26,7 @@ import kotlin.time.ExperimentalTime
@OptIn(ExperimentalTime::class) @OptIn(ExperimentalTime::class)
fun App( fun App(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
selectionManager: SelectionManager, rootSelectionManager: SelectionManager<*>,
selectDirectly: Boolean,
spacing: Dp, spacing: Dp,
game: Game, game: Game,
onNewGame: () -> Unit, onNewGame: () -> Unit,
@@ -57,12 +55,9 @@ fun App(
Box(modifier = modifier) { Box(modifier = modifier) {
AdaptiveGameLayout( AdaptiveGameLayout(
modifier = Modifier.blurOnFinished(isSolved), modifier = Modifier.blurOnFinished(isSolved),
grid = { selectionManager = rootSelectionManager,
val focusable = remember { selectionManager.add() } grid = { selectionManager ->
PuzzleGrid( PuzzleGrid(
modifier = Modifier
.focus(focusable),
selectDirectly = selectDirectly,
selectionManager = selectionManager, selectionManager = selectionManager,
grid = game.grid, grid = game.grid,
spacing = spacing, spacing = spacing,
@@ -72,12 +67,11 @@ fun App(
} }
) )
}, },
horizontalClues = { horizontalClues = { selectionManager ->
val focusable = remember { selectionManager.add() }
for (clue in horizontalClues) { for (clue in horizontalClues) {
HorizontalClue( HorizontalClue(
modifier = Modifier modifier = Modifier
.focus(focusable) .focus(remember { selectionManager.add() })
.forClue(clue, spacing), .forClue(clue, spacing),
spacing = spacing, spacing = spacing,
clue = clue.clue, clue = clue.clue,
@@ -85,23 +79,24 @@ fun App(
) )
} }
}, },
verticalClues = { verticalClues = {selectionManager ->
val focusable = remember { selectionManager.add() } if (verticalClues.isNotEmpty()) {
for (clue in verticalClues) { for (clue in verticalClues) {
VerticalClue( VerticalClue(
modifier = Modifier modifier = Modifier
.focus(focusable) .focus(remember { selectionManager.add() })
.forClue(clue, spacing), .forClue(clue, spacing),
spacing = spacing, spacing = spacing,
clue = clue.clue, clue = clue.clue,
isClueViolated = clue.isViolated isClueViolated = clue.isViolated
) )
}
} }
}, },
time = { time = {
Text( Text(
time, time,
fontSize = TextUnit(4f, TextUnitType.Companion.Em), fontSize = TextUnit(4f, TextUnitType.Em),
textAlign = TextAlign.End textAlign = TextAlign.End
) )
}, },

View File

@@ -1,8 +1,11 @@
package ch.dissem.yaep.ui.common package ch.dissem.yaep.ui.common
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.Placeable 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.LANDSCAPE
import ch.dissem.yaep.ui.common.AspectRatio.PORTRAIT import ch.dissem.yaep.ui.common.AspectRatio.PORTRAIT
import ch.dissem.yaep.ui.common.AspectRatio.SQUARISH 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.max
import kotlin.math.min import kotlin.math.min
@@ -39,37 +47,74 @@ private enum class AspectRatio {
@Composable @Composable
fun AdaptiveGameLayout( fun AdaptiveGameLayout(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
grid: @Composable () -> Unit, selectionManager: SelectionManager<*>,
horizontalClues: @Composable () -> Unit, grid: @Composable (GridSelectionManager) -> Unit,
verticalClues: @Composable () -> Unit, horizontalClues: @Composable (SelectionManager<*>) -> Unit,
verticalClues: @Composable (SelectionManager<*>) -> Unit,
time: @Composable () -> Unit, time: @Composable () -> Unit,
divider: @Composable () -> Unit = { HorizontalDivider() }, divider: @Composable () -> Unit = { HorizontalDivider() },
spacing: Dp = 8.dp 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( 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 modifier = modifier
) { measurables, constraints -> ) { measurables, constraints ->
layout(width = constraints.maxWidth, height = constraints.maxHeight) { layout(width = constraints.maxWidth, height = constraints.maxHeight) {
val aspectRatio = AspectRatio.from(constraints) val aspectRatio = AspectRatio.from(constraints)
val gridMeasurable = measurables[0][0] val gridBoxMeasurable = measurables[0][0]
val horizontalCluesMeasurables = measurables[1] val horizontalCluesBoxMeasurable = measurables[1][0]
val verticalCluesMeasurables = measurables[2] val verticalCluesBoxMeasurable = measurables[2][0]
val timeMeasurable = measurables[3][0]
val dividerMeasurable = measurables[4][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() val spacingPx = spacing.roundToPx()
when (aspectRatio) { when (aspectRatio) {
PORTRAIT -> { PORTRAIT -> {
val divider2Measurable = measurables[5][0] val divider2Measurable = measurables[8][0]
portrait( portrait(
constraints, constraints,
spacingPx, spacingPx,
gridMeasurable, gridMeasurable,
gridBoxMeasurable,
horizontalCluesMeasurables, horizontalCluesMeasurables,
horizontalCluesBoxMeasurable,
verticalCluesMeasurables, verticalCluesMeasurables,
verticalCluesBoxMeasurable,
timeMeasurable, timeMeasurable,
dividerMeasurable, dividerMeasurable,
divider2Measurable divider2Measurable
@@ -81,8 +126,11 @@ fun AdaptiveGameLayout(
constraints, constraints,
spacingPx, spacingPx,
gridMeasurable, gridMeasurable,
gridBoxMeasurable,
horizontalCluesMeasurables, horizontalCluesMeasurables,
horizontalCluesBoxMeasurable,
verticalCluesMeasurables, verticalCluesMeasurables,
verticalCluesBoxMeasurable,
timeMeasurable timeMeasurable
) )
} }
@@ -92,8 +140,11 @@ fun AdaptiveGameLayout(
constraints, constraints,
spacingPx, spacingPx,
gridMeasurable, gridMeasurable,
gridBoxMeasurable,
horizontalCluesMeasurables, horizontalCluesMeasurables,
horizontalCluesBoxMeasurable,
verticalCluesMeasurables, verticalCluesMeasurables,
verticalCluesBoxMeasurable,
timeMeasurable, timeMeasurable,
dividerMeasurable dividerMeasurable
) )
@@ -107,8 +158,11 @@ private fun Placeable.PlacementScope.portrait(
constraints: Constraints, constraints: Constraints,
spacingPx: Int, spacingPx: Int,
gridMeasurable: Measurable, gridMeasurable: Measurable,
gridBoxMeasurable: Measurable,
horizontalCluesMeasurables: List<Measurable>, horizontalCluesMeasurables: List<Measurable>,
horizontalCluesBoxMeasurable: Measurable,
verticalCluesMeasurables: List<Measurable>, verticalCluesMeasurables: List<Measurable>,
verticalCluesBoxMeasurable: Measurable,
timeMeasurable: Measurable, timeMeasurable: Measurable,
divider1Measurable: Measurable, divider1Measurable: Measurable,
divider2Measurable: Measurable divider2Measurable: Measurable
@@ -129,6 +183,7 @@ private fun Placeable.PlacementScope.portrait(
val dividerConstraints = fixedWidth(gridSize) val dividerConstraints = fixedWidth(gridSize)
val gridPlaceable = gridMeasurable.measure(gridConstraints) val gridPlaceable = gridMeasurable.measure(gridConstraints)
val gridBoxPlaceable = gridBoxMeasurable.measure(gridConstraints)
val horizontalCluesPlaceables = horizontalCluesMeasurables.map { val horizontalCluesPlaceables = horizontalCluesMeasurables.map {
it.measure(horizontalCluesConstraints) it.measure(horizontalCluesConstraints)
} }
@@ -139,29 +194,53 @@ private fun Placeable.PlacementScope.portrait(
val divider1Placeable = divider1Measurable.measure(dividerConstraints) val divider1Placeable = divider1Measurable.measure(dividerConstraints)
val divider2Placeable = divider2Measurable.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 // Position the grid
gridPlaceable.place(0, 0) gridPlaceable.place(0, 0)
gridBoxPlaceable.place(0, 0)
divider1Placeable.place(0, gridSize + spacingPx) divider1Placeable.place(0, gridSize + spacingPx)
// Position the horizontal clues // Position the horizontal clues
val horizontalCluesOffsetY = gridSize + 2 * spacingPx
var offsetY = placeClues( var offsetY = placeClues(
placeables = horizontalCluesPlaceables, placeables = horizontalCluesPlaceables,
offsetX = 0, offsetX = 0,
offsetY = gridSize + 2 * spacingPx, offsetY = horizontalCluesOffsetY,
maxWidth = gridSize maxWidth = gridSize
) )
horizontalCluesBoxPlaceable.place(0, horizontalCluesOffsetY)
verticalCluesBoxPlaceable.place(0, horizontalCluesOffsetY)
// Add divider in between // Add divider in between
divider2Placeable.place(0, offsetY + spacingPx) divider2Placeable.place(0, offsetY + spacingPx)
// Position the vertical clues // Position the vertical clues
val verticalCluesOffsetY = offsetY + spacingPx + divider2Placeable.height
offsetY = placeClues( offsetY = placeClues(
placeables = verticalCluesPlaceables, placeables = verticalCluesPlaceables,
offsetX = 0, offsetX = 0,
offsetY = offsetY + spacingPx + divider2Placeable.height, offsetY = verticalCluesOffsetY,
maxWidth = gridSize maxWidth = gridSize
) )
verticalCluesBoxPlaceable.place(0, verticalCluesOffsetY)
// Position the time // Position the time
val remainingSpace = constraints.maxHeight - offsetY val remainingSpace = constraints.maxHeight - offsetY
@@ -190,8 +269,11 @@ private fun Placeable.PlacementScope.squarish(
constraints: Constraints, constraints: Constraints,
spacingPx: Int, spacingPx: Int,
gridMeasurable: Measurable, gridMeasurable: Measurable,
gridBoxMeasurable: Measurable,
horizontalCluesMeasurables: List<Measurable>, horizontalCluesMeasurables: List<Measurable>,
horizontalCluesBoxMeasurable: Measurable,
verticalCluesMeasurables: List<Measurable>, verticalCluesMeasurables: List<Measurable>,
verticalCluesBoxMeasurable: Measurable,
timeMeasurable: Measurable timeMeasurable: Measurable
) { ) {
val gridSize = (7 * min(constraints.maxWidth, constraints.maxHeight)) / 10 val gridSize = (7 * min(constraints.maxWidth, constraints.maxHeight)) / 10
@@ -210,32 +292,55 @@ private fun Placeable.PlacementScope.squarish(
val timeConstraints = Constraints() val timeConstraints = Constraints()
val gridPlaceable = gridMeasurable.measure(gridConstraints) val gridPlaceable = gridMeasurable.measure(gridConstraints)
val gridBoxPlaceable = gridBoxMeasurable.measure(gridConstraints)
val horizontalCluesPlaceables = horizontalCluesMeasurables.map { val horizontalCluesPlaceables = horizontalCluesMeasurables.map {
it.measure(horizontalCluesConstraints) it.measure(horizontalCluesConstraints)
} }
val horizontalCluesBoxPlaceable = horizontalCluesBoxMeasurable
.measure(
cluesBoxConstraints(
width = rightBarWidth,
itemConstraints = horizontalCluesConstraints,
itemCount = horizontalCluesMeasurables.size
)
)
val verticalCluesPlaceables = verticalCluesMeasurables.map { val verticalCluesPlaceables = verticalCluesMeasurables.map {
it.measure(verticalCluesConstraints) it.measure(verticalCluesConstraints)
} }
val verticalCluesBoxPlaceable = verticalCluesBoxMeasurable
.measure(
cluesBoxConstraints(
width = rightBarWidth,
itemConstraints = verticalCluesConstraints,
itemCount = verticalCluesMeasurables.size
)
)
val timePlaceable = timeMeasurable.measure(timeConstraints) val timePlaceable = timeMeasurable.measure(timeConstraints)
// Position the grid // Position the grid
gridPlaceable.place(0, 0) gridPlaceable.place(0, 0)
gridBoxPlaceable.place(0, 0)
// Position the horizontal clues // Position the horizontal clues
val horizontalCluesOffsetX = gridSize + 2 * spacingPx
placeClues( placeClues(
placeables = horizontalCluesPlaceables, placeables = horizontalCluesPlaceables,
offsetX = gridSize + 2 * spacingPx, offsetX = horizontalCluesOffsetX,
offsetY = 0, offsetY = 0,
maxWidth = rightBarWidth maxWidth = rightBarWidth
) )
horizontalCluesBoxPlaceable.place(horizontalCluesOffsetX, 0)
// Position the vertical clues // Position the vertical clues
val verticalCluesOffsetY = gridSize + 2 * spacingPx
placeClues( placeClues(
placeables = verticalCluesPlaceables, placeables = verticalCluesPlaceables,
offsetX = 0, offsetX = 0,
offsetY = gridSize + 2 * spacingPx, offsetY = verticalCluesOffsetY,
maxWidth = gridSize maxWidth = gridSize
) )
verticalCluesBoxPlaceable.place(0, verticalCluesOffsetY)
// Position the time // Position the time
timePlaceable.place( timePlaceable.place(
@@ -248,8 +353,11 @@ private fun Placeable.PlacementScope.landscape(
constraints: Constraints, constraints: Constraints,
spacingPx: Int, spacingPx: Int,
gridMeasurable: Measurable, gridMeasurable: Measurable,
gridBoxMeasurable: Measurable,
horizontalCluesMeasurables: List<Measurable>, horizontalCluesMeasurables: List<Measurable>,
horizontalCluesBoxMeasurable: Measurable,
verticalCluesMeasurables: List<Measurable>, verticalCluesMeasurables: List<Measurable>,
verticalCluesBoxMeasurable: Measurable,
timeMeasurable: Measurable, timeMeasurable: Measurable,
dividerMeasurable: Measurable dividerMeasurable: Measurable
) { ) {
@@ -271,36 +379,61 @@ private fun Placeable.PlacementScope.landscape(
val dividerConstraints = fixedWidth(rightBarWidth - 2 * spacingPx) val dividerConstraints = fixedWidth(rightBarWidth - 2 * spacingPx)
val gridPlaceable = gridMeasurable.measure(gridConstraints) val gridPlaceable = gridMeasurable.measure(gridConstraints)
val gridBoxPlaceable = gridBoxMeasurable.measure(gridConstraints)
val horizontalCluesPlaceables = horizontalCluesMeasurables.map { val horizontalCluesPlaceables = horizontalCluesMeasurables.map {
it.measure(horizontalCluesConstraints) it.measure(horizontalCluesConstraints)
} }
val horizontalCluesBoxPlaceable = horizontalCluesBoxMeasurable
.measure(
cluesBoxConstraints(
width = rightBarWidth,
itemConstraints = horizontalCluesConstraints,
itemCount = horizontalCluesMeasurables.size
)
)
val verticalCluesPlaceables = verticalCluesMeasurables.map { val verticalCluesPlaceables = verticalCluesMeasurables.map {
it.measure(verticalCluesConstraints) it.measure(verticalCluesConstraints)
} }
val verticalCluesBoxPlaceable = verticalCluesBoxMeasurable
.measure(
cluesBoxConstraints(
width = rightBarWidth,
itemConstraints = verticalCluesConstraints,
itemCount = verticalCluesMeasurables.size
)
)
val timePlaceable = timeMeasurable.measure(timeConstraints) val timePlaceable = timeMeasurable.measure(timeConstraints)
val dividerPlaceable = dividerMeasurable.measure(dividerConstraints) val dividerPlaceable = dividerMeasurable.measure(dividerConstraints)
// Position the grid // Position the grid
gridPlaceable.place(0, 0) gridPlaceable.place(0, 0)
gridBoxPlaceable.place(0, 0)
// Position the horizontal clues // Position the horizontal clues4
val horizontalCluesOffsetX = gridSize + 2 * spacingPx
val offsetY = placeClues( val offsetY = placeClues(
placeables = horizontalCluesPlaceables, placeables = horizontalCluesPlaceables,
offsetX = gridSize + 2 * spacingPx, offsetX = horizontalCluesOffsetX,
offsetY = 0, offsetY = 0,
maxWidth = rightBarWidth maxWidth = rightBarWidth
) )
horizontalCluesBoxPlaceable.place(horizontalCluesOffsetX, 0)
// Add divider in between // Add divider in between
dividerPlaceable.place(gridSize + 3 * spacingPx, offsetY + spacingPx) dividerPlaceable.place(gridSize + 3 * spacingPx, offsetY + spacingPx)
// Position the vertical clues // Position the vertical clues
val verticalCluesOffsetX = gridSize + 2 * spacingPx
val verticalCluesOffsetY = offsetY + spacingPx + dividerPlaceable.height
placeClues( placeClues(
placeables = verticalCluesPlaceables, placeables = verticalCluesPlaceables,
offsetX = gridSize + 2 * spacingPx, offsetX = verticalCluesOffsetX,
offsetY = offsetY + spacingPx + dividerPlaceable.height, offsetY = verticalCluesOffsetY,
maxWidth = rightBarWidth maxWidth = rightBarWidth
) )
verticalCluesBoxPlaceable.place(verticalCluesOffsetX, verticalCluesOffsetY)
// Position the time // Position the time
timePlaceable.place( 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( private fun Placeable.PlacementScope.placeClues(
placeables: List<Placeable>, placeables: List<Placeable>,
offsetX: Int, offsetX: Int,

View File

@@ -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<FocusFollowingFocusable>(manager) {
val focusRequester = FocusRequester()
fun setFocus(state: FocusState) {
if (state.hasFocus) {
manager.focused = this
} else if (manager.focused === this) {
manager.focused = null
}
}
}

View File

@@ -0,0 +1,17 @@
package ch.dissem.yaep.ui.common.focus
import androidx.compose.ui.input.key.KeyEvent
object FocusFollowingSelectionManager : SelectionManager<FocusFollowingFocusable>() {
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
}

View File

@@ -0,0 +1,21 @@
package ch.dissem.yaep.ui.common.focus
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
abstract class Focusable<F : Focusable<F>>(protected val manager: SelectionManager<F>) {
val hasFocus: Flow<Boolean> =
combine(manager.isActiveFlow, manager.focusedFlow) { isActive, focused ->
isActive && focused == this
}
var child: SelectionManager<*>? = null
private set
fun <M : SelectionManager<*>> create(selectionManager: M): M = selectionManager.apply {
isActive = manager.isActive && manager.focused == this@Focusable
this@Focusable.child = this
}
}

View File

@@ -0,0 +1,3 @@
package ch.dissem.yaep.ui.common.focus
class GridFocusable(manager: GridSelectionManager) : Focusable<GridFocusable>(manager)

View File

@@ -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<GridFocusable>() {
val grid = mutableListOf<MutableList<GridFocusable>>()
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
}
}

View File

@@ -0,0 +1,17 @@
package ch.dissem.yaep.ui.common.focus
class LinearFocusable(manager: LinearSelectionManager) : Focusable<LinearFocusable>(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
}
}

View File

@@ -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<LinearFocusable>() {
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
}
}

View File

@@ -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<F : Focusable<F>> {
var isActiveFlow = MutableStateFlow<Boolean>(false)
var isActive: Boolean
get() = isActiveFlow.value
set(value) {
isActiveFlow.value = value
}
var focusedFlow = MutableStateFlow<F?>(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
}

View File

@@ -1,6 +1,5 @@
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
@@ -13,7 +12,6 @@ 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 androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ch.dissem.yaep.domain.GameCell 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.Grid
import ch.dissem.yaep.domain.Item import ch.dissem.yaep.domain.Item
import ch.dissem.yaep.domain.ItemClass import ch.dissem.yaep.domain.ItemClass
import focus import ch.dissem.yaep.ui.common.focus.GridSelectionManager
@Composable @Composable
fun PuzzleGrid( fun PuzzleGrid(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
selectDirectly: Boolean, selectionManager: GridSelectionManager,
selectionManager: SelectionManager,
spacing: Dp = 8.dp, spacing: Dp = 8.dp,
grid: Grid, grid: Grid,
onUpdate: () -> Unit onUpdate: () -> Unit
@@ -40,8 +37,7 @@ fun PuzzleGrid(
onSnapshot = { grid.snapshot() }, onSnapshot = { grid.snapshot() },
onUndo = { grid.undo() }, onUndo = { grid.undo() },
spacing = spacing, spacing = spacing,
selectDirectly = selectDirectly, selectionManager = remember { selectionManager.addRow() }
selectionManager = selectionManager
) )
} }
} }
@@ -54,20 +50,15 @@ private fun PuzzleRow(
onSnapshot: () -> Unit, onSnapshot: () -> Unit,
onUndo: () -> Boolean, onUndo: () -> Boolean,
spacing: Dp, spacing: Dp,
selectDirectly: Boolean, selectionManager: GridSelectionManager
selectionManager: SelectionManager
) { ) {
val focusableRow = remember { selectionManager.add() }
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentHeight() .wrapContentHeight()
) { ) {
val allOptions = row.options val allOptions = row.options
val columnSelectionManager =
remember { focusableRow.createChild(Key.DirectionRight, Key.DirectionLeft) }
for (cell in row) { for (cell in row) {
val focusableItem = remember { columnSelectionManager.add() }
var selection by remember(cell) { mutableStateOf(cell.selection) } var selection by remember(cell) { mutableStateOf(cell.selection) }
val options = remember(cell) { val options = remember(cell) {
allOptions.map { Toggleable(it, cell.options.contains(it)) } allOptions.map { Toggleable(it, cell.options.contains(it)) }
@@ -83,11 +74,10 @@ private fun PuzzleRow(
} }
Selector( Selector(
modifier = Modifier modifier = Modifier
.focus(focusableItem) .focus(remember { selectionManager.add() })
.padding(spacing) .padding(spacing)
.weight(1f), .weight(1f),
spacing, spacing,
selectDirectly = selectDirectly,
options = options, options = options,
onOptionRemoved = { onOptionRemoved = {
onSnapshot() onSnapshot()

View File

@@ -13,16 +13,15 @@ import yaep.commonui.generated.resources.Res
import yaep.commonui.generated.resources.ant import yaep.commonui.generated.resources.ant
import yaep.commonui.generated.resources.astronaut import yaep.commonui.generated.resources.astronaut
import yaep.commonui.generated.resources.banana import yaep.commonui.generated.resources.banana
import yaep.commonui.generated.resources.bubble_tea
import yaep.commonui.generated.resources.beverage import yaep.commonui.generated.resources.beverage
import yaep.commonui.generated.resources.bicycle import yaep.commonui.generated.resources.bicycle
import yaep.commonui.generated.resources.british import yaep.commonui.generated.resources.british
import yaep.commonui.generated.resources.bubble_tea
import yaep.commonui.generated.resources.bus import yaep.commonui.generated.resources.bus
import yaep.commonui.generated.resources.cake import yaep.commonui.generated.resources.cake
import yaep.commonui.generated.resources.canadian import yaep.commonui.generated.resources.canadian
import yaep.commonui.generated.resources.cherries import yaep.commonui.generated.resources.cherries
import yaep.commonui.generated.resources.chocolate import yaep.commonui.generated.resources.chocolate
import yaep.commonui.generated.resources.water
import yaep.commonui.generated.resources.coffee import yaep.commonui.generated.resources.coffee
import yaep.commonui.generated.resources.cookie import yaep.commonui.generated.resources.cookie
import yaep.commonui.generated.resources.cupcake 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.locomotive
import yaep.commonui.generated.resources.lollipop import yaep.commonui.generated.resources.lollipop
import yaep.commonui.generated.resources.mango import yaep.commonui.generated.resources.mango
import yaep.commonui.generated.resources.mate
import yaep.commonui.generated.resources.milk import yaep.commonui.generated.resources.milk
import yaep.commonui.generated.resources.motor_scooter import yaep.commonui.generated.resources.motor_scooter
import yaep.commonui.generated.resources.norwegian 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.teacher
import yaep.commonui.generated.resources.tram_car import yaep.commonui.generated.resources.tram_car
import yaep.commonui.generated.resources.ukrainian import yaep.commonui.generated.resources.ukrainian
import yaep.commonui.generated.resources.water
import yaep.commonui.generated.resources.watermelon import yaep.commonui.generated.resources.watermelon
import yaep.commonui.generated.resources.mate
import yaep.commonui.generated.resources.zebra import yaep.commonui.generated.resources.zebra
val ItemClass<*>.localName: StringResource val ItemClass<*>.localName: StringResource

View File

@@ -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<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
}
}
}
internal fun Modifier.forClue(clue: DisplayClue<out Clue>): 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()
}
}

View File

@@ -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)
}
}
}
)
}
}
}
}
}

View File

@@ -20,6 +20,7 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import ch.dissem.yaep.domain.Item import ch.dissem.yaep.domain.Item
import ch.dissem.yaep.domain.ItemClass import ch.dissem.yaep.domain.ItemClass
import ch.dissem.yaep.ui.common.theme.emojiFontFamily import ch.dissem.yaep.ui.common.theme.emojiFontFamily
@@ -28,8 +29,7 @@ import kotlin.math.min
@Composable @Composable
fun <C : ItemClass<C>> Selector( fun <C : ItemClass<C>> Selector(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
spacing: Dp, spacing: Dp = 4.dp,
selectDirectly: Boolean,
options: List<Toggleable<Item<C>>>, options: List<Toggleable<Item<C>>>,
onOptionRemoved: (Item<C>) -> Unit, onOptionRemoved: (Item<C>) -> Unit,
onOptionAdded: (Item<C>) -> Unit, onOptionAdded: (Item<C>) -> Unit,
@@ -83,8 +83,8 @@ fun <C : ItemClass<C>> Selector(
@Composable @Composable
fun <C : ItemClass<C>> DrawItem( fun <C : ItemClass<C>> DrawItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
spacing: Dp, item: Item<C>,
item: Item<C> spacing: Dp = 4.dp
) { ) {
OutlinedCard(modifier = modifier.aspectRatio(1f), shape = RoundedCornerShape(spacing)) { OutlinedCard(modifier = modifier.aspectRatio(1f), shape = RoundedCornerShape(spacing)) {
val emoji = item.symbol val emoji = item.symbol

View File

@@ -1,4 +1,5 @@
package ch.dissem.yaep.ui.common.theme package ch.dissem.yaep.ui.common.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
val primaryLight = Color(0xFF6D5E0F) val primaryLight = Color(0xFF6D5E0F)

View File

@@ -1,128 +1,39 @@
package ch.dissem.yaep.ui.common
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.focusable
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier 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.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 androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow import ch.dissem.yaep.ui.common.focus.FocusFollowingFocusable
import kotlinx.coroutines.flow.MutableStateFlow import ch.dissem.yaep.ui.common.focus.Focusable
import kotlinx.coroutines.flow.combine
import kotlin.IllegalStateException
class SelectionManager(
val keyNext: Key,
val keyPrevious: Key? = null
) {
var isActiveFlow = MutableStateFlow<Boolean>(false)
var isActive: Boolean
get() = isActiveFlow.value
set(value) {
isActiveFlow.value = value
}
var focusedFlow = MutableStateFlow<Focusable?>(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<Boolean> =
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!!
}
}
@Composable @Composable
fun Modifier.focus(holder: Focusable): Modifier = if (holder.hasFocus.collectAsState(false).value) { fun Modifier.focus(holder: Focusable<*>): Modifier {
border( var m = this
width = 2.dp, val hasFocus by holder.hasFocus.collectAsState(false)
color = MaterialTheme.colorScheme.primary, if (hasFocus) {
shape = RectangleShape m = m.border(
) width = 2.dp,
} else { color = MaterialTheme.colorScheme.primary,
this shape = RectangleShape
)
}
if (holder is FocusFollowingFocusable) {
m = m
.focusRequester(holder.focusRequester)
.onFocusEvent { state ->
holder.setFocus(state)
}
.onFocusChanged {}
.focusable()
}
return m
} }

View File

@@ -1,6 +1,5 @@
package ch.dissem.yaep.ui.desktop package ch.dissem.yaep.ui.desktop
import SelectionManager
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -17,12 +16,12 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.WindowPlacement
import androidx.compose.ui.window.WindowScope import androidx.compose.ui.window.WindowScope
import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.WindowState
import ch.dissem.yaep.ui.common.focus.FocusFollowingSelectionManager
import ch.dissem.yaep.ui.common.theme.AppTheme import ch.dissem.yaep.ui.common.theme.AppTheme
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -43,7 +42,7 @@ import yaep.desktop.generated.resources.Res as DRes
@Composable @Composable
fun WindowScope.DesktopWindow( fun WindowScope.DesktopWindow(
useDarkMode: Boolean, useDarkMode: Boolean,
selectionManager: SelectionManager, selectionManager: FocusFollowingSelectionManager,
topBar: @Composable () -> Unit, topBar: @Composable () -> Unit,
content: @Composable (PaddingValues) -> Unit content: @Composable (PaddingValues) -> Unit
) { ) {

View File

@@ -1,13 +1,11 @@
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
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.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -20,6 +18,7 @@ import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState import androidx.compose.ui.window.rememberWindowState
import ch.dissem.yaep.domain.generateGame import ch.dissem.yaep.domain.generateGame
import ch.dissem.yaep.ui.common.App import ch.dissem.yaep.ui.common.App
import ch.dissem.yaep.ui.common.focus.FocusFollowingSelectionManager
import ch.dissem.yaep.ui.common.theme.emojiFontFamily import ch.dissem.yaep.ui.common.theme.emojiFontFamily
import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
@@ -49,12 +48,12 @@ fun main(): Unit = application {
state = windowState, state = windowState,
icon = painterResource(DRes.drawable.ic_launcher) 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 useDarkMode by remember { mutableStateOf(true) }
var resetCluesBeacon by remember { mutableStateOf(Any()) } var resetCluesBeacon by remember { mutableStateOf(Any()) }
DesktopWindow( DesktopWindow(
useDarkMode = useDarkMode, useDarkMode = useDarkMode,
selectionManager = selectionManager, selectionManager = rootSelectionManager,
topBar = { topBar = {
AppBar( AppBar(
useDarkMode = useDarkMode, useDarkMode = useDarkMode,
@@ -71,9 +70,8 @@ fun main(): Unit = application {
) { ) {
App( App(
modifier = Modifier.padding(it), modifier = Modifier.padding(it),
selectionManager = selectionManager, rootSelectionManager = rootSelectionManager,
spacing = 8.dp, spacing = 8.dp,
selectDirectly = true,
game = game, game = game,
onNewGame = { game = generateGame() }, onNewGame = { game = generateGame() },
resetCluesBeacon = resetCluesBeacon resetCluesBeacon = resetCluesBeacon