Add Keyboard Control (WIP)

This commit is contained in:
2025-12-25 11:35:45 +01:00
committed by Christian Basler
parent 3582196720
commit c6f7cbae2b
20 changed files with 357 additions and 104 deletions

View File

@@ -73,7 +73,16 @@ fun App(
for (clue in horizontalClues) { for (clue in horizontalClues) {
HorizontalClue( HorizontalClue(
modifier = Modifier modifier = Modifier
.focus(remember { selectionManager.add() }) .focus(remember(selectionManager) {
selectionManager.add(
primaryAction = {
clue.isActive = !clue.isActive
},
secondaryAction = {
clue.isActive = false
}
)
})
.forClue(clue, spacing), .forClue(clue, spacing),
spacing = spacing, spacing = spacing,
clue = clue.clue, clue = clue.clue,
@@ -86,7 +95,16 @@ fun App(
for (clue in verticalClues) { for (clue in verticalClues) {
VerticalClue( VerticalClue(
modifier = Modifier modifier = Modifier
.focus(remember { selectionManager.add() }) .focus(remember(selectionManager) {
selectionManager.add(
primaryAction = {
clue.isActive = !clue.isActive
},
secondaryAction = {
clue.isActive = false
}
)
})
.forClue(clue, spacing), .forClue(clue, spacing),
spacing = spacing, spacing = spacing,
clue = clue.clue, clue = clue.clue,
@@ -102,7 +120,8 @@ fun App(
textAlign = TextAlign.End textAlign = TextAlign.End
) )
}, },
spacing = spacing spacing = spacing,
game, resetCluesBeacon
) )
EndOfGame(isSolved = isSolved, time = time, onRestart = onNewGame) EndOfGame(isSolved = isSolved, time = time, onRestart = onNewGame)
} }

View File

@@ -0,0 +1,7 @@
package ch.dissem.yaep.ui.common.focus
class CluesFocusable(
manager: CluesSelectionManager,
primaryAction: (() -> Unit)?,
secondaryAction: (() -> Unit)?
) : Focusable<CluesFocusable>(manager, primaryAction, secondaryAction)

View File

@@ -0,0 +1,72 @@
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.key
import ch.dissem.yaep.ui.common.ceilDiv
import kotlin.math.max
import kotlin.math.min
class CluesSelectionManager : SelectionManager<CluesFocusable>() {
var columns: Int = 1
private var row: Int = 0
private var col: Int = 0
private val focusables = mutableListOf<CluesFocusable>()
override fun add(
primaryAction: (() -> Unit)?,
secondaryAction: (() -> Unit)?
): CluesFocusable {
val new = CluesFocusable(
manager = this,
primaryAction = primaryAction,
secondaryAction = secondaryAction
)
focusables.add(new)
return new
}
override fun doOnKeyEvent(event: KeyEvent): Boolean {
val rows = focusables.size ceilDiv columns
when (event.key) {
Key.DirectionDown -> {
row++
if (row >= rows) {
row = 0
}
}
Key.DirectionUp -> {
row--
if (row < 0) {
row = rows - 1
}
}
Key.DirectionRight -> {
col++
if (col >= columns) {
col = 0
}
}
Key.DirectionLeft -> {
col--
if (col < 0) {
col = columns - 1
}
}
else -> return focused?.child?.onKeyEvent(event) == true
}
// This makes sure the limits aren't exceeded when values are changed concurrently
val index = max(0, min(row * columns + col, focusables.size - 1))
focused = focusables[index]
return true
}
}

View File

@@ -3,8 +3,11 @@ package ch.dissem.yaep.ui.common.focus
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.FocusState import androidx.compose.ui.focus.FocusState
class FocusFollowingFocusable(manager: FocusFollowingSelectionManager) : class FocusFollowingFocusable(
Focusable<FocusFollowingFocusable>(manager) { manager: FocusFollowingSelectionManager,
primaryAction: (() -> Unit)?,
secondaryAction: (() -> Unit)?
) : Focusable<FocusFollowingFocusable>(manager, primaryAction, secondaryAction) {
val focusRequester = FocusRequester() val focusRequester = FocusRequester()

View File

@@ -7,11 +7,18 @@ object FocusFollowingSelectionManager : SelectionManager<FocusFollowingFocusable
isActive = true isActive = true
} }
override fun add(): FocusFollowingFocusable { override fun add(
return FocusFollowingFocusable(this) primaryAction: (() -> Unit)?,
secondaryAction: (() -> Unit)?
): FocusFollowingFocusable {
return FocusFollowingFocusable(
manager = this,
primaryAction = primaryAction,
secondaryAction = secondaryAction
)
} }
// Key events are ignored, the default focus mechanisms are used // Key events are ignored, the default focus mechanisms are used
override fun onKeyEvent(event: KeyEvent): Boolean = false override fun doOnKeyEvent(event: KeyEvent): Boolean = focused?.child?.onKeyEvent(event) == true
} }

View File

@@ -1,9 +1,15 @@
package ch.dissem.yaep.ui.common.focus package ch.dissem.yaep.ui.common.focus
import androidx.compose.ui.input.key.KeyEvent
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
abstract class Focusable<F : Focusable<F>>(protected val manager: SelectionManager<F>) { abstract class Focusable<F : Focusable<F>>(
protected val manager: SelectionManager<F>,
val primaryAction: (() -> Unit)?,
val secondaryAction: (() -> Unit)?,
var onKeyEvent: ((KeyEvent) -> Boolean)? = null
) {
val hasFocus: Flow<Boolean> = val hasFocus: Flow<Boolean> =
combine(manager.isActiveFlow, manager.focusedFlow) { isActive, focused -> combine(manager.isActiveFlow, manager.focusedFlow) { isActive, focused ->

View File

@@ -1,3 +1,10 @@
package ch.dissem.yaep.ui.common.focus package ch.dissem.yaep.ui.common.focus
class GridFocusable(manager: GridSelectionManager) : Focusable<GridFocusable>(manager) import androidx.compose.ui.input.key.KeyEvent
class GridFocusable(
manager: GridSelectionManager,
primaryAction: (() -> Unit)?,
secondaryAction: (() -> Unit)?,
onKeyEvent: ((KeyEvent) -> Boolean)? = null
) : Focusable<GridFocusable>(manager, primaryAction, secondaryAction, onKeyEvent)

View File

@@ -2,9 +2,7 @@ package ch.dissem.yaep.ui.common.focus
import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent 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.key
import androidx.compose.ui.input.key.type
class GridSelectionManager : SelectionManager<GridFocusable>() { class GridSelectionManager : SelectionManager<GridFocusable>() {
@@ -18,14 +16,32 @@ class GridSelectionManager : SelectionManager<GridFocusable>() {
return this return this
} }
override fun add(): GridFocusable { fun add(onKeyEvent: (KeyEvent) -> Boolean): GridFocusable {
val new = GridFocusable(this) val new = GridFocusable(
manager = this,
primaryAction = null,
secondaryAction = null,
onKeyEvent = onKeyEvent
)
grid.last().add(new) grid.last().add(new)
return new return new
} }
override fun onKeyEvent(event: KeyEvent): Boolean { override fun add(
if (event.type != KeyEventType.KeyUp) return false primaryAction: (() -> Unit)?,
secondaryAction: (() -> Unit)?
): GridFocusable {
val new = GridFocusable(
manager = this,
primaryAction = primaryAction,
secondaryAction = secondaryAction
)
grid.last().add(new)
return new
}
override fun doOnKeyEvent(event: KeyEvent): Boolean {
if (grid.isEmpty()) return false
when (event.key) { when (event.key) {
Key.DirectionDown -> { Key.DirectionDown -> {

View File

@@ -1,6 +1,10 @@
package ch.dissem.yaep.ui.common.focus package ch.dissem.yaep.ui.common.focus
class LinearFocusable(manager: LinearSelectionManager) : Focusable<LinearFocusable>(manager) { class LinearFocusable(
manager: LinearSelectionManager,
primaryAction: (() -> Unit)?,
secondaryAction: (() -> Unit)?
) : Focusable<LinearFocusable>(manager, primaryAction, secondaryAction) {
var previous: LinearFocusable = this var previous: LinearFocusable = this
private set private set

View File

@@ -2,10 +2,8 @@ package ch.dissem.yaep.ui.common.focus
import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent 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.isShiftPressed
import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.type
class LinearSelectionManager( class LinearSelectionManager(
val keyNext: Key, val keyNext: Key,
@@ -20,8 +18,15 @@ class LinearSelectionManager(
focused = focused?.previous focused = focused?.previous
} }
override fun add(): LinearFocusable { override fun add(
val new = LinearFocusable(this) primaryAction: (() -> Unit)?,
secondaryAction: (() -> Unit)?
): LinearFocusable {
val new = LinearFocusable(
manager = this,
primaryAction = primaryAction,
secondaryAction = secondaryAction
)
if (focused != null) { if (focused != null) {
new.next = focused!! new.next = focused!!
} else { } else {
@@ -30,9 +35,7 @@ class LinearSelectionManager(
return new return new
} }
override fun onKeyEvent(event: KeyEvent): Boolean { override fun doOnKeyEvent(event: KeyEvent): Boolean {
if (event.type != KeyEventType.KeyUp) return false
if (event.key == keyNext) { if (event.key == keyNext) {
if (keyPrevious == null && event.isShiftPressed) { if (keyPrevious == null && event.isShiftPressed) {
focusPrevious() focusPrevious()

View File

@@ -1,6 +1,10 @@
package ch.dissem.yaep.ui.common.focus 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.KeyEvent
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.type
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
abstract class SelectionManager<F : Focusable<F>> { abstract class SelectionManager<F : Focusable<F>> {
@@ -24,6 +28,34 @@ abstract class SelectionManager<F : Focusable<F>> {
} }
} }
abstract fun add(): F fun add(): F = add(null, null)
abstract fun onKeyEvent(event: KeyEvent): Boolean
fun add(primaryAction: () -> Unit): F = add(primaryAction, null)
abstract fun add(
primaryAction: (() -> Unit)?,
secondaryAction: (() -> Unit)?
): F
fun onKeyEvent(event: KeyEvent): Boolean {
if (event.type != KeyEventType.KeyUp) return false
return when (event.key) {
Key.Spacebar, Key.Enter -> {
focused?.primaryAction?.invoke() != null ||
focused?.onKeyEvent?.invoke(event) == true ||
focused?.child?.onKeyEvent(event) == true
}
Key.Delete, Key.Backspace -> {
focused?.secondaryAction?.invoke() != null ||
focused?.onKeyEvent?.invoke(event) == true ||
focused?.child?.onKeyEvent(event) == true
}
else -> focused?.onKeyEvent?.invoke(event) == true || doOnKeyEvent(event)
}
}
protected abstract fun doOnKeyEvent(event: KeyEvent): Boolean
} }

View File

@@ -12,6 +12,10 @@ 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.input.key.KeyEvent
import androidx.compose.ui.input.key.isShiftPressed
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
@@ -37,7 +41,7 @@ fun PuzzleGrid(
onSnapshot = { grid.snapshot() }, onSnapshot = { grid.snapshot() },
onUndo = { grid.undo() }, onUndo = { grid.undo() },
spacing = spacing, spacing = spacing,
selectionManager = remember { selectionManager.addRow() } selectionManager = remember(selectionManager) { selectionManager.addRow() }
) )
} }
} }
@@ -72,17 +76,42 @@ private fun PuzzleRow(
onUpdate() onUpdate()
} }
} }
val focusable = remember(cell, selectionManager) {
selectionManager.add { e ->
if (selection != null) {
when (e.key) {
Key.Spacebar, Key.Enter, Key.Delete, Key.Backspace -> {
selection = null
true
}
else -> false
}
} else {
val i = getNumber(e)
if (i != null && i in 1..options.size) {
val selectedItem = options[i - 1].item
if (e.isShiftPressed) {
onOptionRemoved(row, cell, selectedItem, onSnapshot)
} else {
onSelectItem(row, cell, options, selectedItem, onSnapshot, onUndo)
}
true
} else {
false
}
}
}
}
Selector( Selector(
modifier = Modifier modifier = Modifier
.focus(remember { selectionManager.add() }) .focus(focusable)
.padding(spacing) .padding(spacing)
.weight(1f), .weight(1f),
spacing, spacing,
options = options, options = options,
onOptionRemoved = { onOptionRemoved = { selectedItem ->
onSnapshot() onOptionRemoved(row, cell, selectedItem, onSnapshot)
cell.options.remove(it)
row.cleanupOptions()
}, },
onOptionAdded = { onOptionAdded = {
cell.options.add(it) cell.options.add(it)
@@ -96,6 +125,31 @@ private fun PuzzleRow(
} }
} }
private fun onOptionRemoved(
row: GameRow<ItemClass<*>>,
cell: GameCell<ItemClass<*>>,
selectedItem: Item<ItemClass<*>>?,
onSnapshot: () -> Unit
) {
onSnapshot()
cell.options.remove(selectedItem)
row.cleanupOptions()
}
private fun getNumber(e: KeyEvent): Int? = when (e.key) {
Key.Zero, Key.NumPad0 -> 0
Key.One, Key.NumPad1 -> 1
Key.Two, Key.NumPad2 -> 2
Key.Three, Key.NumPad3 -> 3
Key.Four, Key.NumPad4 -> 4
Key.Five, Key.NumPad5 -> 5
Key.Six, Key.NumPad6 -> 6
Key.Seven, Key.NumPad7 -> 7
Key.Eight, Key.NumPad8 -> 8
Key.Nine, Key.NumPad9 -> 9
else -> null
}
private fun onSelectItem( private fun onSelectItem(
row: GameRow<ItemClass<*>>, row: GameRow<ItemClass<*>>,
cell: GameCell<ItemClass<*>>, cell: GameCell<ItemClass<*>>,

View File

@@ -1,13 +1,17 @@
package ch.dissem.yaep.ui.common.layout package ch.dissem.yaep.ui.common.layout
import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.Measurable
import ch.dissem.yaep.ui.common.focus.CluesSelectionManager
data class FocusGroup( data class FocusGroup(
val items: List<Measurable>, val items: List<Measurable>,
val box: Measurable val box: Measurable,
val selectionManger: CluesSelectionManager? = null
) { ) {
val item: Measurable val item: Measurable
get() = items.single() get() = items.single()
val hasItems: Boolean = items.isNotEmpty()
val count = items.size val count = items.size
} }

View File

@@ -2,17 +2,17 @@ package ch.dissem.yaep.ui.common.layout
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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.Placeable import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Constraints.Companion.fixed import androidx.compose.ui.unit.Constraints.Companion.fixed
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.ui.common.focus.CluesSelectionManager
import ch.dissem.yaep.ui.common.focus.FocusFollowingFocusable import ch.dissem.yaep.ui.common.focus.FocusFollowingFocusable
import ch.dissem.yaep.ui.common.focus.GridSelectionManager 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 ch.dissem.yaep.ui.common.focus.SelectionManager
import ch.dissem.yaep.ui.common.layout.AspectRatio.LANDSCAPE import ch.dissem.yaep.ui.common.layout.AspectRatio.LANDSCAPE
import ch.dissem.yaep.ui.common.layout.AspectRatio.PORTRAIT import ch.dissem.yaep.ui.common.layout.AspectRatio.PORTRAIT
@@ -29,20 +29,21 @@ fun AdaptiveGameLayout(
horizontalClues: @Composable (SelectionManager<*>) -> Unit, horizontalClues: @Composable (SelectionManager<*>) -> Unit,
verticalClues: @Composable (SelectionManager<*>) -> Unit, verticalClues: @Composable (SelectionManager<*>) -> Unit,
time: @Composable () -> Unit, time: @Composable () -> Unit,
spacing: Dp = 8.dp spacing: Dp = 8.dp,
vararg resetBeacons: Any
) { ) {
val gridFocusable = selectionManager.add() val gridFocusable = remember(*resetBeacons) { selectionManager.add() }
val gridSelectionManager = gridFocusable.create(GridSelectionManager()) val gridSelectionManager = remember(*resetBeacons) { gridFocusable.create(GridSelectionManager()) }
val horizontalCluesFocusable = selectionManager.add() val horizontalCluesFocusable = remember(*resetBeacons) { selectionManager.add() }
val horizontalCluesSelectionManager = horizontalCluesFocusable.create( val horizontalCluesSelectionManager = remember(*resetBeacons) {
LinearSelectionManager(Key.DirectionRight, Key.DirectionLeft) horizontalCluesFocusable.create(CluesSelectionManager())
) }
val verticalCluesFocusable = selectionManager.add() val verticalCluesFocusable = remember(*resetBeacons) { selectionManager.add() }
val verticalCluesSelectionManager = verticalCluesFocusable.create( val verticalCluesSelectionManager = remember(*resetBeacons) {
LinearSelectionManager(Key.DirectionRight, Key.DirectionLeft) verticalCluesFocusable.create(CluesSelectionManager())
) }
Layout( Layout(
contents = listOf( contents = listOf(
{ grid(gridSelectionManager) }, { grid(gridSelectionManager) },
@@ -63,16 +64,18 @@ fun AdaptiveGameLayout(
val aspectRatio = AspectRatio.from(constraints) val aspectRatio = AspectRatio.from(constraints)
val gridGroup = FocusGroup( val gridGroup = FocusGroup(
measurables[0], items = measurables[0],
measurables[1][0], box = measurables[1][0],
) )
val horizontalCluesGroup = FocusGroup( val horizontalCluesGroup = FocusGroup(
measurables[2], items = measurables[2],
measurables[3][0] box = measurables[3][0],
selectionManger = horizontalCluesSelectionManager
) )
val verticalCluesGroup = FocusGroup( val verticalCluesGroup = FocusGroup(
measurables[4], items = measurables[4],
measurables[5][0] box = measurables[5][0],
selectionManger = verticalCluesSelectionManager
) )
val timeMeasurable = measurables[6][0] val timeMeasurable = measurables[6][0]
val dividerMeasurables = measurables[7] val dividerMeasurables = measurables[7]
@@ -122,11 +125,15 @@ fun AdaptiveGameLayout(
internal fun cluesBoxConstraints( internal fun cluesBoxConstraints(
width: Int, width: Int,
itemConstraints: Constraints, itemConstraints: Constraints,
itemCount: Int group: FocusGroup
): Constraints = fixed( ): Constraints {
width, val columns = width / itemConstraints.maxWidth
itemConstraints.maxHeight * ceil(itemCount.toFloat() / (width / itemConstraints.maxWidth)).toInt() group.selectionManger?.columns = columns
) return fixed(
width,
itemConstraints.maxHeight * ceil(group.count.toFloat() / columns).toInt()
)
}
internal fun Placeable.PlacementScope.placeClues( internal fun Placeable.PlacementScope.placeClues(
placeables: List<Placeable>, placeables: List<Placeable>,

View File

@@ -42,7 +42,7 @@ internal fun Placeable.PlacementScope.landscape(
cluesBoxConstraints( cluesBoxConstraints(
width = rightBarWidth, width = rightBarWidth,
itemConstraints = horizontalCluesConstraints, itemConstraints = horizontalCluesConstraints,
itemCount = horizontalClues.count group = horizontalClues
) )
) )
@@ -54,7 +54,7 @@ internal fun Placeable.PlacementScope.landscape(
cluesBoxConstraints( cluesBoxConstraints(
width = rightBarWidth, width = rightBarWidth,
itemConstraints = verticalCluesConstraints, itemConstraints = verticalCluesConstraints,
itemCount = verticalClues.count group = verticalClues
) )
) )
@@ -75,19 +75,21 @@ internal fun Placeable.PlacementScope.landscape(
maxWidth = rightBarWidth maxWidth = rightBarWidth
) )
// Add divider in between if (verticalClues.hasItems) {
dividerPlaceable.place(gridSize + 3 * spacingPx, offsetY + spacingPx) // Add divider in between
dividerPlaceable.place(gridSize + 3 * spacingPx, offsetY + spacingPx)
// Position the vertical clues // Position the vertical clues
val verticalCluesOffsetX = gridSize + 2 * spacingPx val verticalCluesOffsetX = gridSize + 2 * spacingPx
val verticalCluesOffsetY = offsetY + spacingPx + dividerPlaceable.height val verticalCluesOffsetY = offsetY + spacingPx + dividerPlaceable.height
verticalCluesBoxPlaceable.place(verticalCluesOffsetX, verticalCluesOffsetY) verticalCluesBoxPlaceable.place(verticalCluesOffsetX, verticalCluesOffsetY)
placeClues( placeClues(
placeables = verticalCluesPlaceables, placeables = verticalCluesPlaceables,
offsetX = verticalCluesOffsetX, offsetX = verticalCluesOffsetX,
offsetY = verticalCluesOffsetY, offsetY = verticalCluesOffsetY,
maxWidth = rightBarWidth maxWidth = rightBarWidth
) )
}
// Position the time // Position the time
timePlaceable.place( timePlaceable.place(

View File

@@ -47,7 +47,7 @@ internal fun Placeable.PlacementScope.portrait(
cluesBoxConstraints( cluesBoxConstraints(
width = gridSize, width = gridSize,
itemConstraints = horizontalCluesConstraints, itemConstraints = horizontalCluesConstraints,
itemCount = horizontalClues.count group = horizontalClues
) )
) )
val verticalCluesBoxPlaceable = verticalClues.box val verticalCluesBoxPlaceable = verticalClues.box
@@ -55,7 +55,7 @@ internal fun Placeable.PlacementScope.portrait(
cluesBoxConstraints( cluesBoxConstraints(
width = gridSize, width = gridSize,
itemConstraints = verticalCluesConstraints, itemConstraints = verticalCluesConstraints,
itemCount = verticalClues.count group = verticalClues
) )
) )
@@ -76,20 +76,21 @@ internal fun Placeable.PlacementScope.portrait(
maxWidth = gridSize maxWidth = gridSize
) )
// Add divider in between if (verticalClues.hasItems) {
divider2Placeable.place(0, offsetY + spacingPx) // Add divider in between
divider2Placeable.place(0, offsetY + spacingPx)
// Position the vertical clues
val verticalCluesOffsetY = offsetY + spacingPx + divider2Placeable.height
verticalCluesBoxPlaceable.place(0, verticalCluesOffsetY)
offsetY = placeClues(
placeables = verticalCluesPlaceables,
offsetX = 0,
offsetY = verticalCluesOffsetY,
maxWidth = gridSize
)
verticalCluesBoxPlaceable.place(0, verticalCluesOffsetY)
// Position the vertical clues
val verticalCluesOffsetY = offsetY + spacingPx + divider2Placeable.height
verticalCluesBoxPlaceable.place(0, verticalCluesOffsetY)
offsetY = placeClues(
placeables = verticalCluesPlaceables,
offsetX = 0,
offsetY = verticalCluesOffsetY,
maxWidth = gridSize
)
verticalCluesBoxPlaceable.place(0, verticalCluesOffsetY)
}
// Position the time // Position the time
val remainingSpace = constraints.maxHeight - offsetY val remainingSpace = constraints.maxHeight - offsetY
if (remainingSpace < timePlaceable.height) { if (remainingSpace < timePlaceable.height) {

View File

@@ -40,7 +40,7 @@ internal fun Placeable.PlacementScope.squarish(
cluesBoxConstraints( cluesBoxConstraints(
width = rightBarWidth, width = rightBarWidth,
itemConstraints = horizontalCluesConstraints, itemConstraints = horizontalCluesConstraints,
itemCount = horizontalClues.count group = horizontalClues
) )
) )
@@ -52,7 +52,7 @@ internal fun Placeable.PlacementScope.squarish(
cluesBoxConstraints( cluesBoxConstraints(
width = rightBarWidth, width = rightBarWidth,
itemConstraints = verticalCluesConstraints, itemConstraints = verticalCluesConstraints,
itemCount = verticalClues.count group = verticalClues
) )
) )
val timePlaceable = timeMeasurable.measure(timeConstraints) val timePlaceable = timeMeasurable.measure(timeConstraints)
@@ -71,15 +71,17 @@ internal fun Placeable.PlacementScope.squarish(
maxWidth = rightBarWidth maxWidth = rightBarWidth
) )
// Position the vertical clues if (verticalClues.hasItems) {
val verticalCluesOffsetY = gridSize + 2 * spacingPx // Position the vertical clues
verticalCluesBoxPlaceable.place(0, verticalCluesOffsetY) val verticalCluesOffsetY = gridSize + 2 * spacingPx
placeClues( verticalCluesBoxPlaceable.place(0, verticalCluesOffsetY)
placeables = verticalCluesPlaceables, placeClues(
offsetX = 0, placeables = verticalCluesPlaceables,
offsetY = verticalCluesOffsetY, offsetX = 0,
maxWidth = gridSize offsetY = verticalCluesOffsetY,
) maxWidth = gridSize
)
}
// Position the time // Position the time
timePlaceable.place( timePlaceable.place(

View File

@@ -0,0 +1,8 @@
package ch.dissem.yaep.ui.common
import kotlin.math.absoluteValue
import kotlin.math.sign
infix fun Int.ceilDiv(other: Int): Int {
return this.floorDiv(other) + this.rem(other).sign.absoluteValue
}

View File

@@ -1,7 +1,6 @@
package ch.dissem.yaep.ui.common package ch.dissem.yaep.ui.common
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -40,7 +39,7 @@ fun <C : ItemClass<C>> Selector(
if (selectedItem != null) { if (selectedItem != null) {
DrawItem( DrawItem(
item = selectedItem, item = selectedItem,
modifier = modifier.clickable { onSelectItem(null) }, modifier = modifier.onEitherPointerAction { onSelectItem(null) },
spacing = radius spacing = radius
) )
} else { } else {

View File

@@ -16,9 +16,9 @@ import ch.dissem.yaep.ui.common.focus.FocusFollowingFocusable
import ch.dissem.yaep.ui.common.focus.Focusable import ch.dissem.yaep.ui.common.focus.Focusable
@Composable @Composable
fun Modifier.focus(holder: Focusable<*>): Modifier { fun Modifier.focus(focusable: Focusable<*>): Modifier {
var m = this var m = this
val hasFocus by holder.hasFocus.collectAsState(false) val hasFocus by focusable.hasFocus.collectAsState(false)
if (hasFocus) { if (hasFocus) {
m = m.border( m = m.border(
width = 2.dp, width = 2.dp,
@@ -26,11 +26,11 @@ fun Modifier.focus(holder: Focusable<*>): Modifier {
shape = RectangleShape shape = RectangleShape
) )
} }
if (holder is FocusFollowingFocusable) { if (focusable is FocusFollowingFocusable) {
m = m m = m
.focusRequester(holder.focusRequester) .focusRequester(focusable.focusRequester)
.onFocusEvent { state -> .onFocusEvent { state ->
holder.setFocus(state) focusable.setFocus(state)
} }
.onFocusChanged {} .onFocusChanged {}
.focusable() .focusable()