Add keyboard control (WIP)

This commit is contained in:
2025-06-16 00:23:36 +02:00
parent 651c74e305
commit e0de7be857
4 changed files with 118 additions and 19 deletions

View File

@@ -1,5 +1,6 @@
package ch.dissem.yaep.ui.common
import SelectionManager
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -15,6 +16,7 @@ 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.domain.HorizontalClue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -25,6 +27,7 @@ import kotlin.time.ExperimentalTime
@OptIn(ExperimentalTime::class)
fun App(
modifier: Modifier = Modifier,
selectionManager: SelectionManager,
selectDirectly: Boolean,
spacing: Dp,
game: Game,
@@ -56,6 +59,8 @@ fun App(
modifier = Modifier.blurOnFinished(isSolved),
grid = {
PuzzleGrid(
modifier = Modifier
.focus(remember { selectionManager.add() }),
grid = game.grid,
spacing = spacing,
selectDirectly = selectDirectly,
@@ -66,9 +71,12 @@ fun App(
)
},
horizontalClues = {
val horizontalClueSelection = remember { selectionManager.add() }
for (clue in horizontalClues) {
HorizontalClue(
modifier = Modifier.forClue(clue, spacing),
modifier = Modifier
.focus(horizontalClueSelection)
.forClue(clue, spacing),
spacing = spacing,
clue = clue.clue,
isClueViolated = clue.isViolated
@@ -76,9 +84,12 @@ fun App(
}
},
verticalClues = {
val verticalClueSelection = remember { selectionManager.add() }
for (clue in verticalClues) {
VerticalClue(
modifier = Modifier.forClue(clue, spacing),
modifier = Modifier
.focus(verticalClueSelection)
.forClue(clue, spacing),
spacing = spacing,
clue = clue.clue,
isClueViolated = clue.isViolated

View File

@@ -1,39 +1,118 @@
import androidx.compose.foundation.border
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
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 StatusManager {
lateinit var focused: StatusHolder
private set
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) {
focusedFlow.value = value
}
val last: Focusable
get() = focused?.previous ?: throw IllegalStateException("not initialized")
val child: SelectionManager?
get() = focused?.child
fun focusNext() {
focused = focused.next
focused = focused?.next
}
fun focusPrevious() {
focused = focused.previous
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 StatusHolder(private val manager: StatusManager) {
val hasFocus: Boolean
get() = manager.focused == this
var isSelected: Boolean = false
var previous: StatusHolder = this
private set
var next: StatusHolder = this
set(value) {
field = value
value.previous = this
class Focusable(
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 = false
return child!!
}
}
@Composable
fun Modifier.status(holder: StatusHolder): Modifier = if (holder.hasFocus) {
fun Modifier.focus(holder: Focusable): Modifier = if (holder.hasFocus.collectAsState(false).value) {
border(
width = 2.dp,
color = MaterialTheme.colorScheme.primary,