Refactor adaptive layout
This commit is contained in:
@@ -15,7 +15,9 @@ 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 ch.dissem.yaep.ui.common.focus.FocusFollowingFocusable
|
||||||
import ch.dissem.yaep.ui.common.focus.SelectionManager
|
import ch.dissem.yaep.ui.common.focus.SelectionManager
|
||||||
|
import ch.dissem.yaep.ui.common.layout.AdaptiveGameLayout
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.IO
|
import kotlinx.coroutines.IO
|
||||||
@@ -26,7 +28,7 @@ import kotlin.time.ExperimentalTime
|
|||||||
@OptIn(ExperimentalTime::class)
|
@OptIn(ExperimentalTime::class)
|
||||||
fun App(
|
fun App(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
rootSelectionManager: SelectionManager<*>,
|
rootSelectionManager: SelectionManager<FocusFollowingFocusable>,
|
||||||
spacing: Dp,
|
spacing: Dp,
|
||||||
game: Game,
|
game: Game,
|
||||||
onNewGame: () -> Unit,
|
onNewGame: () -> Unit,
|
||||||
|
|||||||
@@ -1,480 +0,0 @@
|
|||||||
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
|
|
||||||
import androidx.compose.ui.unit.Constraints
|
|
||||||
import androidx.compose.ui.unit.Constraints.Companion.fixed
|
|
||||||
import androidx.compose.ui.unit.Constraints.Companion.fixedWidth
|
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
private enum class AspectRatio {
|
|
||||||
PORTRAIT, LANDSCAPE, SQUARISH;
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val ASPECT_RATIO_LANDSCAPE = 1.4f
|
|
||||||
private const val ASPECT_RATIO_PORTRAIT = 1 / ASPECT_RATIO_LANDSCAPE
|
|
||||||
|
|
||||||
fun from(constraints: Constraints): AspectRatio {
|
|
||||||
val ratio = constraints.maxWidth.toFloat() / constraints.maxHeight.toFloat()
|
|
||||||
return when {
|
|
||||||
ratio < ASPECT_RATIO_PORTRAIT -> PORTRAIT
|
|
||||||
ratio > ASPECT_RATIO_LANDSCAPE -> LANDSCAPE
|
|
||||||
else -> SQUARISH
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun AdaptiveGameLayout(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
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(
|
|
||||||
{
|
|
||||||
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 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[8][0]
|
|
||||||
portrait(
|
|
||||||
constraints,
|
|
||||||
spacingPx,
|
|
||||||
gridMeasurable,
|
|
||||||
gridBoxMeasurable,
|
|
||||||
horizontalCluesMeasurables,
|
|
||||||
horizontalCluesBoxMeasurable,
|
|
||||||
verticalCluesMeasurables,
|
|
||||||
verticalCluesBoxMeasurable,
|
|
||||||
timeMeasurable,
|
|
||||||
dividerMeasurable,
|
|
||||||
divider2Measurable
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
SQUARISH -> {
|
|
||||||
squarish(
|
|
||||||
constraints,
|
|
||||||
spacingPx,
|
|
||||||
gridMeasurable,
|
|
||||||
gridBoxMeasurable,
|
|
||||||
horizontalCluesMeasurables,
|
|
||||||
horizontalCluesBoxMeasurable,
|
|
||||||
verticalCluesMeasurables,
|
|
||||||
verticalCluesBoxMeasurable,
|
|
||||||
timeMeasurable
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
LANDSCAPE -> {
|
|
||||||
landscape(
|
|
||||||
constraints,
|
|
||||||
spacingPx,
|
|
||||||
gridMeasurable,
|
|
||||||
gridBoxMeasurable,
|
|
||||||
horizontalCluesMeasurables,
|
|
||||||
horizontalCluesBoxMeasurable,
|
|
||||||
verticalCluesMeasurables,
|
|
||||||
verticalCluesBoxMeasurable,
|
|
||||||
timeMeasurable,
|
|
||||||
dividerMeasurable
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Placeable.PlacementScope.portrait(
|
|
||||||
constraints: Constraints,
|
|
||||||
spacingPx: Int,
|
|
||||||
gridMeasurable: Measurable,
|
|
||||||
gridBoxMeasurable: Measurable,
|
|
||||||
horizontalCluesMeasurables: List<Measurable>,
|
|
||||||
horizontalCluesBoxMeasurable: Measurable,
|
|
||||||
verticalCluesMeasurables: List<Measurable>,
|
|
||||||
verticalCluesBoxMeasurable: Measurable,
|
|
||||||
timeMeasurable: Measurable,
|
|
||||||
divider1Measurable: Measurable,
|
|
||||||
divider2Measurable: Measurable
|
|
||||||
) {
|
|
||||||
val gridSize = constraints.maxWidth
|
|
||||||
val gridItemSize = (gridSize - 12 * spacingPx) / 18
|
|
||||||
|
|
||||||
val gridConstraints = fixed(gridSize, gridSize)
|
|
||||||
val horizontalCluesConstraints = fixed(
|
|
||||||
width = 3 * gridItemSize + 2 * spacingPx,
|
|
||||||
height = gridItemSize + 2 * spacingPx
|
|
||||||
)
|
|
||||||
val verticalCluesConstraints = fixed(
|
|
||||||
width = gridItemSize + 2 * spacingPx,
|
|
||||||
height = 3 * gridItemSize + 2 * spacingPx
|
|
||||||
)
|
|
||||||
val timeConstraints = Constraints()
|
|
||||||
val dividerConstraints = fixedWidth(gridSize)
|
|
||||||
|
|
||||||
val gridPlaceable = gridMeasurable.measure(gridConstraints)
|
|
||||||
val gridBoxPlaceable = gridBoxMeasurable.measure(gridConstraints)
|
|
||||||
val horizontalCluesPlaceables = horizontalCluesMeasurables.map {
|
|
||||||
it.measure(horizontalCluesConstraints)
|
|
||||||
}
|
|
||||||
val verticalCluesPlaceables = verticalCluesMeasurables.map {
|
|
||||||
it.measure(verticalCluesConstraints)
|
|
||||||
}
|
|
||||||
val timePlaceable = timeMeasurable.measure(timeConstraints)
|
|
||||||
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 = 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 = verticalCluesOffsetY,
|
|
||||||
maxWidth = gridSize
|
|
||||||
)
|
|
||||||
verticalCluesBoxPlaceable.place(0, verticalCluesOffsetY)
|
|
||||||
|
|
||||||
// Position the time
|
|
||||||
val remainingSpace = constraints.maxHeight - offsetY
|
|
||||||
if (remainingSpace < timePlaceable.height) {
|
|
||||||
val scale = remainingSpace.toFloat() / timePlaceable.height.toFloat()
|
|
||||||
if (scale > 0.1f) {
|
|
||||||
timePlaceable.placeWithLayer(
|
|
||||||
x = constraints.maxWidth - timePlaceable.width - spacingPx,
|
|
||||||
y = constraints.maxHeight - timePlaceable.height
|
|
||||||
) {
|
|
||||||
scaleX = scale
|
|
||||||
scaleY = scale
|
|
||||||
translationX = (timePlaceable.width * (1 - scale)) / 2f
|
|
||||||
translationY = (timePlaceable.height * (1 - scale)) / 2f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
timePlaceable.place(
|
|
||||||
x = constraints.maxWidth - timePlaceable.width - spacingPx,
|
|
||||||
y = constraints.maxHeight - timePlaceable.height
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Placeable.PlacementScope.squarish(
|
|
||||||
constraints: Constraints,
|
|
||||||
spacingPx: Int,
|
|
||||||
gridMeasurable: Measurable,
|
|
||||||
gridBoxMeasurable: Measurable,
|
|
||||||
horizontalCluesMeasurables: List<Measurable>,
|
|
||||||
horizontalCluesBoxMeasurable: Measurable,
|
|
||||||
verticalCluesMeasurables: List<Measurable>,
|
|
||||||
verticalCluesBoxMeasurable: Measurable,
|
|
||||||
timeMeasurable: Measurable
|
|
||||||
) {
|
|
||||||
val gridSize = (7 * min(constraints.maxWidth, constraints.maxHeight)) / 10
|
|
||||||
val gridItemSize = (gridSize - 12 * spacingPx) / 18
|
|
||||||
val rightBarWidth = constraints.maxWidth - gridSize - spacingPx
|
|
||||||
|
|
||||||
val gridConstraints = fixed(gridSize, gridSize)
|
|
||||||
val horizontalCluesConstraints = fixed(
|
|
||||||
width = 3 * gridItemSize + 2 * spacingPx,
|
|
||||||
height = gridItemSize + 2 * spacingPx
|
|
||||||
)
|
|
||||||
val verticalCluesConstraints = fixed(
|
|
||||||
width = gridItemSize + 2 * spacingPx,
|
|
||||||
height = 3 * gridItemSize + 2 * spacingPx
|
|
||||||
)
|
|
||||||
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 = horizontalCluesOffsetX,
|
|
||||||
offsetY = 0,
|
|
||||||
maxWidth = rightBarWidth
|
|
||||||
)
|
|
||||||
horizontalCluesBoxPlaceable.place(horizontalCluesOffsetX, 0)
|
|
||||||
|
|
||||||
// Position the vertical clues
|
|
||||||
val verticalCluesOffsetY = gridSize + 2 * spacingPx
|
|
||||||
placeClues(
|
|
||||||
placeables = verticalCluesPlaceables,
|
|
||||||
offsetX = 0,
|
|
||||||
offsetY = verticalCluesOffsetY,
|
|
||||||
maxWidth = gridSize
|
|
||||||
)
|
|
||||||
verticalCluesBoxPlaceable.place(0, verticalCluesOffsetY)
|
|
||||||
|
|
||||||
// Position the time
|
|
||||||
timePlaceable.place(
|
|
||||||
x = constraints.maxWidth - timePlaceable.width - spacingPx,
|
|
||||||
y = constraints.maxHeight - timePlaceable.height
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Placeable.PlacementScope.landscape(
|
|
||||||
constraints: Constraints,
|
|
||||||
spacingPx: Int,
|
|
||||||
gridMeasurable: Measurable,
|
|
||||||
gridBoxMeasurable: Measurable,
|
|
||||||
horizontalCluesMeasurables: List<Measurable>,
|
|
||||||
horizontalCluesBoxMeasurable: Measurable,
|
|
||||||
verticalCluesMeasurables: List<Measurable>,
|
|
||||||
verticalCluesBoxMeasurable: Measurable,
|
|
||||||
timeMeasurable: Measurable,
|
|
||||||
dividerMeasurable: Measurable
|
|
||||||
) {
|
|
||||||
val gridSize = constraints.maxHeight
|
|
||||||
val gridItemSize = (gridSize - 12 * spacingPx) / 18
|
|
||||||
val rightBarWidth = constraints.maxWidth - gridSize - 2 * spacingPx
|
|
||||||
|
|
||||||
val gridConstraints = fixed(gridSize, gridSize)
|
|
||||||
val baseSpace = gridSize - 2 * spacingPx
|
|
||||||
val horizontalCluesConstraints = fixed(
|
|
||||||
width = 3 * gridItemSize + 2 * spacingPx,
|
|
||||||
height = gridItemSize + 2 * spacingPx
|
|
||||||
)
|
|
||||||
val verticalCluesConstraints = fixed(
|
|
||||||
width = gridItemSize + 2 * spacingPx,
|
|
||||||
height = 3 * gridItemSize + 2 * spacingPx
|
|
||||||
)
|
|
||||||
val timeConstraints = Constraints.fixedHeight(baseSpace / 10)
|
|
||||||
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 clues4
|
|
||||||
val horizontalCluesOffsetX = gridSize + 2 * spacingPx
|
|
||||||
val offsetY = placeClues(
|
|
||||||
placeables = horizontalCluesPlaceables,
|
|
||||||
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 = verticalCluesOffsetX,
|
|
||||||
offsetY = verticalCluesOffsetY,
|
|
||||||
maxWidth = rightBarWidth
|
|
||||||
)
|
|
||||||
verticalCluesBoxPlaceable.place(verticalCluesOffsetX, verticalCluesOffsetY)
|
|
||||||
|
|
||||||
// Position the time
|
|
||||||
timePlaceable.place(
|
|
||||||
x = constraints.maxWidth - timePlaceable.width - spacingPx,
|
|
||||||
y = constraints.maxHeight - timePlaceable.height
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Placeable>,
|
|
||||||
offsetX: Int,
|
|
||||||
offsetY: Int,
|
|
||||||
maxWidth: Int
|
|
||||||
): Int {
|
|
||||||
if (placeables.isEmpty()) return offsetY
|
|
||||||
|
|
||||||
val itemWidth = placeables.first().width
|
|
||||||
val itemHeight = placeables.first().height
|
|
||||||
val columns = max(1, maxWidth / itemWidth)
|
|
||||||
val spacing = if (columns == 1) 0 else (maxWidth - columns * itemWidth) / (columns - 1)
|
|
||||||
var currentX = offsetX
|
|
||||||
var currentY = offsetY
|
|
||||||
var i = 0
|
|
||||||
for (placeable in placeables) {
|
|
||||||
placeable.place(currentX, currentY)
|
|
||||||
currentX += itemWidth + spacing
|
|
||||||
i++
|
|
||||||
if (i % columns == 0 && i < placeables.size) {
|
|
||||||
currentX = offsetX
|
|
||||||
currentY += itemHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return currentY + itemHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package ch.dissem.yaep.ui.common.layout
|
||||||
|
|
||||||
|
import androidx.compose.ui.unit.Constraints
|
||||||
|
|
||||||
|
enum class AspectRatio {
|
||||||
|
PORTRAIT, LANDSCAPE, SQUARISH;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ASPECT_RATIO_LANDSCAPE = 1.4f
|
||||||
|
private const val ASPECT_RATIO_PORTRAIT = 1 / ASPECT_RATIO_LANDSCAPE
|
||||||
|
|
||||||
|
fun from(constraints: Constraints): AspectRatio {
|
||||||
|
val ratio = constraints.maxWidth.toFloat() / constraints.maxHeight.toFloat()
|
||||||
|
return when {
|
||||||
|
ratio < ASPECT_RATIO_PORTRAIT -> PORTRAIT
|
||||||
|
ratio > ASPECT_RATIO_LANDSCAPE -> LANDSCAPE
|
||||||
|
else -> SQUARISH
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package ch.dissem.yaep.ui.common.layout
|
||||||
|
|
||||||
|
import androidx.compose.ui.layout.Measurable
|
||||||
|
|
||||||
|
data class FocusGroup(
|
||||||
|
val items: List<Measurable>,
|
||||||
|
val box: Measurable
|
||||||
|
) {
|
||||||
|
val item: Measurable
|
||||||
|
get() = items.single()
|
||||||
|
|
||||||
|
val count = items.size
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package ch.dissem.yaep.ui.common.layout
|
||||||
|
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.key.Key
|
||||||
|
import androidx.compose.ui.layout.Layout
|
||||||
|
import androidx.compose.ui.layout.Placeable
|
||||||
|
import androidx.compose.ui.unit.Constraints
|
||||||
|
import androidx.compose.ui.unit.Constraints.Companion.fixed
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
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 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.SQUARISH
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AdaptiveGameLayout(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
selectionManager: SelectionManager<FocusFollowingFocusable>,
|
||||||
|
grid: @Composable (GridSelectionManager) -> Unit,
|
||||||
|
horizontalClues: @Composable (SelectionManager<*>) -> Unit,
|
||||||
|
verticalClues: @Composable (SelectionManager<*>) -> Unit,
|
||||||
|
time: @Composable () -> Unit,
|
||||||
|
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(gridSelectionManager) },
|
||||||
|
{ FocusBox(gridFocusable, requestFocus = true) },
|
||||||
|
{ horizontalClues(horizontalCluesSelectionManager) },
|
||||||
|
{ FocusBox(horizontalCluesFocusable) },
|
||||||
|
{ verticalClues(verticalCluesSelectionManager) },
|
||||||
|
{ FocusBox(verticalCluesFocusable) },
|
||||||
|
time,
|
||||||
|
{
|
||||||
|
HorizontalDivider()
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
modifier = modifier
|
||||||
|
) { measurables, constraints ->
|
||||||
|
layout(width = constraints.maxWidth, height = constraints.maxHeight) {
|
||||||
|
val aspectRatio = AspectRatio.from(constraints)
|
||||||
|
|
||||||
|
val gridGroup = FocusGroup(
|
||||||
|
measurables[0],
|
||||||
|
measurables[1][0],
|
||||||
|
)
|
||||||
|
val horizontalCluesGroup = FocusGroup(
|
||||||
|
measurables[2],
|
||||||
|
measurables[3][0]
|
||||||
|
)
|
||||||
|
val verticalCluesGroup = FocusGroup(
|
||||||
|
measurables[4],
|
||||||
|
measurables[5][0]
|
||||||
|
)
|
||||||
|
val timeMeasurable = measurables[6][0]
|
||||||
|
val dividerMeasurables = measurables[7]
|
||||||
|
|
||||||
|
val spacingPx = spacing.roundToPx()
|
||||||
|
|
||||||
|
when (aspectRatio) {
|
||||||
|
PORTRAIT -> {
|
||||||
|
portrait(
|
||||||
|
constraints,
|
||||||
|
spacingPx,
|
||||||
|
gridGroup,
|
||||||
|
horizontalCluesGroup,
|
||||||
|
verticalCluesGroup,
|
||||||
|
timeMeasurable,
|
||||||
|
dividerMeasurables
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SQUARISH -> {
|
||||||
|
squarish(
|
||||||
|
constraints,
|
||||||
|
spacingPx,
|
||||||
|
gridGroup,
|
||||||
|
horizontalCluesGroup,
|
||||||
|
verticalCluesGroup,
|
||||||
|
timeMeasurable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LANDSCAPE -> {
|
||||||
|
landscape(
|
||||||
|
constraints,
|
||||||
|
spacingPx,
|
||||||
|
gridGroup,
|
||||||
|
horizontalCluesGroup,
|
||||||
|
verticalCluesGroup,
|
||||||
|
timeMeasurable,
|
||||||
|
dividerMeasurables[0]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun cluesBoxConstraints(
|
||||||
|
width: Int,
|
||||||
|
itemConstraints: Constraints,
|
||||||
|
itemCount: Int
|
||||||
|
): Constraints = fixed(
|
||||||
|
width,
|
||||||
|
itemConstraints.maxHeight * ceil(itemCount.toFloat() / (width / itemConstraints.maxWidth)).toInt()
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun Placeable.PlacementScope.placeClues(
|
||||||
|
placeables: List<Placeable>,
|
||||||
|
offsetX: Int,
|
||||||
|
offsetY: Int,
|
||||||
|
maxWidth: Int
|
||||||
|
): Int {
|
||||||
|
if (placeables.isEmpty()) return offsetY
|
||||||
|
|
||||||
|
val itemWidth = placeables.first().width
|
||||||
|
val itemHeight = placeables.first().height
|
||||||
|
val columns = max(1, maxWidth / itemWidth)
|
||||||
|
val spacing = if (columns == 1) 0 else (maxWidth - columns * itemWidth) / (columns - 1)
|
||||||
|
var currentX = offsetX
|
||||||
|
var currentY = offsetY
|
||||||
|
var i = 0
|
||||||
|
for (placeable in placeables) {
|
||||||
|
placeable.place(currentX, currentY)
|
||||||
|
currentX += itemWidth + spacing
|
||||||
|
i++
|
||||||
|
if (i % columns == 0 && i < placeables.size) {
|
||||||
|
currentX = offsetX
|
||||||
|
currentY += itemHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return currentY + itemHeight
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package ch.dissem.yaep.ui.common.layout
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.focusable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.focus.onFocusEvent
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import ch.dissem.yaep.ui.common.focus.FocusFollowingFocusable
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FocusBox(focusable: FocusFollowingFocusable, requestFocus: Boolean = false) {
|
||||||
|
val hasFocus by focusable.hasFocus.collectAsState(false)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.focusRequester(focusable.focusRequester)
|
||||||
|
.onFocusEvent { state ->
|
||||||
|
focusable.setFocus(state)
|
||||||
|
}
|
||||||
|
.onFocusChanged {}
|
||||||
|
.focusable()
|
||||||
|
.clip(MaterialTheme.shapes.small)
|
||||||
|
.background(
|
||||||
|
if (hasFocus) {
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
|
} else {
|
||||||
|
Color.Transparent
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if (requestFocus) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
focusable.focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package ch.dissem.yaep.ui.common.layout
|
||||||
|
|
||||||
|
import androidx.compose.ui.layout.Measurable
|
||||||
|
import androidx.compose.ui.layout.Placeable
|
||||||
|
import androidx.compose.ui.unit.Constraints
|
||||||
|
import androidx.compose.ui.unit.Constraints.Companion.fixed
|
||||||
|
import androidx.compose.ui.unit.Constraints.Companion.fixedWidth
|
||||||
|
|
||||||
|
internal fun Placeable.PlacementScope.landscape(
|
||||||
|
constraints: Constraints,
|
||||||
|
spacingPx: Int,
|
||||||
|
grid: FocusGroup,
|
||||||
|
horizontalClues: FocusGroup,
|
||||||
|
verticalClues: FocusGroup,
|
||||||
|
timeMeasurable: Measurable,
|
||||||
|
dividerMeasurable: Measurable
|
||||||
|
) {
|
||||||
|
val gridSize = constraints.maxHeight
|
||||||
|
val gridItemSize = (gridSize - 12 * spacingPx) / 18
|
||||||
|
val rightBarWidth = constraints.maxWidth - gridSize - 2 * spacingPx
|
||||||
|
|
||||||
|
val gridConstraints = fixed(gridSize, gridSize)
|
||||||
|
val baseSpace = gridSize - 2 * spacingPx
|
||||||
|
val horizontalCluesConstraints = fixed(
|
||||||
|
width = 3 * gridItemSize + 2 * spacingPx,
|
||||||
|
height = gridItemSize + 2 * spacingPx
|
||||||
|
)
|
||||||
|
val verticalCluesConstraints = fixed(
|
||||||
|
width = gridItemSize + 2 * spacingPx,
|
||||||
|
height = 3 * gridItemSize + 2 * spacingPx
|
||||||
|
)
|
||||||
|
val timeConstraints = Constraints.fixedHeight(baseSpace / 10)
|
||||||
|
val dividerConstraints = fixedWidth(rightBarWidth - 2 * spacingPx)
|
||||||
|
|
||||||
|
val gridPlaceable = grid.item.measure(gridConstraints)
|
||||||
|
val gridBoxPlaceable = grid.box.measure(gridConstraints)
|
||||||
|
val horizontalCluesPlaceables = horizontalClues.items.map {
|
||||||
|
it.measure(horizontalCluesConstraints)
|
||||||
|
}
|
||||||
|
val horizontalCluesBoxPlaceable = horizontalClues.box
|
||||||
|
.measure(
|
||||||
|
cluesBoxConstraints(
|
||||||
|
width = rightBarWidth,
|
||||||
|
itemConstraints = horizontalCluesConstraints,
|
||||||
|
itemCount = horizontalClues.count
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val verticalCluesPlaceables = verticalClues.items.map {
|
||||||
|
it.measure(verticalCluesConstraints)
|
||||||
|
}
|
||||||
|
val verticalCluesBoxPlaceable = verticalClues.box
|
||||||
|
.measure(
|
||||||
|
cluesBoxConstraints(
|
||||||
|
width = rightBarWidth,
|
||||||
|
itemConstraints = verticalCluesConstraints,
|
||||||
|
itemCount = verticalClues.count
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val timePlaceable = timeMeasurable.measure(timeConstraints)
|
||||||
|
val dividerPlaceable = dividerMeasurable.measure(dividerConstraints)
|
||||||
|
|
||||||
|
// Position the grid
|
||||||
|
gridBoxPlaceable.place(0, 0)
|
||||||
|
gridPlaceable.place(0, 0)
|
||||||
|
|
||||||
|
// Position the horizontal clues4
|
||||||
|
val horizontalCluesOffsetX = gridSize + 2 * spacingPx
|
||||||
|
horizontalCluesBoxPlaceable.place(horizontalCluesOffsetX, 0)
|
||||||
|
val offsetY = placeClues(
|
||||||
|
placeables = horizontalCluesPlaceables,
|
||||||
|
offsetX = horizontalCluesOffsetX,
|
||||||
|
offsetY = 0,
|
||||||
|
maxWidth = rightBarWidth
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
verticalCluesBoxPlaceable.place(verticalCluesOffsetX, verticalCluesOffsetY)
|
||||||
|
placeClues(
|
||||||
|
placeables = verticalCluesPlaceables,
|
||||||
|
offsetX = verticalCluesOffsetX,
|
||||||
|
offsetY = verticalCluesOffsetY,
|
||||||
|
maxWidth = rightBarWidth
|
||||||
|
)
|
||||||
|
|
||||||
|
// Position the time
|
||||||
|
timePlaceable.place(
|
||||||
|
x = constraints.maxWidth - timePlaceable.width - spacingPx,
|
||||||
|
y = constraints.maxHeight - timePlaceable.height
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package ch.dissem.yaep.ui.common.layout
|
||||||
|
|
||||||
|
import androidx.compose.ui.layout.Measurable
|
||||||
|
import androidx.compose.ui.layout.Placeable
|
||||||
|
import androidx.compose.ui.unit.Constraints
|
||||||
|
import androidx.compose.ui.unit.Constraints.Companion.fixed
|
||||||
|
import androidx.compose.ui.unit.Constraints.Companion.fixedWidth
|
||||||
|
|
||||||
|
internal fun Placeable.PlacementScope.portrait(
|
||||||
|
constraints: Constraints,
|
||||||
|
spacingPx: Int,
|
||||||
|
grid: FocusGroup,
|
||||||
|
horizontalClues: FocusGroup,
|
||||||
|
verticalClues: FocusGroup,
|
||||||
|
timeMeasurable: Measurable,
|
||||||
|
dividerMeasurables: List<Measurable>
|
||||||
|
) {
|
||||||
|
val gridSize = constraints.maxWidth
|
||||||
|
val gridItemSize = (gridSize - 12 * spacingPx) / 18
|
||||||
|
|
||||||
|
val gridConstraints = fixed(gridSize, gridSize)
|
||||||
|
val horizontalCluesConstraints = fixed(
|
||||||
|
width = 3 * gridItemSize + 2 * spacingPx,
|
||||||
|
height = gridItemSize + 2 * spacingPx
|
||||||
|
)
|
||||||
|
val verticalCluesConstraints = fixed(
|
||||||
|
width = gridItemSize + 2 * spacingPx,
|
||||||
|
height = 3 * gridItemSize + 2 * spacingPx
|
||||||
|
)
|
||||||
|
val timeConstraints = Constraints()
|
||||||
|
val dividerConstraints = fixedWidth(gridSize)
|
||||||
|
|
||||||
|
val gridPlaceable = grid.item.measure(gridConstraints)
|
||||||
|
val gridBoxPlaceable = grid.box.measure(gridConstraints)
|
||||||
|
val horizontalCluesPlaceables = horizontalClues.items.map {
|
||||||
|
it.measure(horizontalCluesConstraints)
|
||||||
|
}
|
||||||
|
val verticalCluesPlaceables = verticalClues.items.map {
|
||||||
|
it.measure(verticalCluesConstraints)
|
||||||
|
}
|
||||||
|
val timePlaceable = timeMeasurable.measure(timeConstraints)
|
||||||
|
val divider1Placeable = dividerMeasurables[0].measure(dividerConstraints)
|
||||||
|
val divider2Placeable = dividerMeasurables[1].measure(dividerConstraints)
|
||||||
|
|
||||||
|
val horizontalCluesBoxPlaceable = horizontalClues.box
|
||||||
|
.measure(
|
||||||
|
cluesBoxConstraints(
|
||||||
|
width = gridSize,
|
||||||
|
itemConstraints = horizontalCluesConstraints,
|
||||||
|
itemCount = horizontalClues.count
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val verticalCluesBoxPlaceable = verticalClues.box
|
||||||
|
.measure(
|
||||||
|
cluesBoxConstraints(
|
||||||
|
width = gridSize,
|
||||||
|
itemConstraints = verticalCluesConstraints,
|
||||||
|
itemCount = verticalClues.count
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Position the grid
|
||||||
|
gridBoxPlaceable.place(0, 0)
|
||||||
|
gridPlaceable.place(0, 0)
|
||||||
|
|
||||||
|
divider1Placeable.place(0, gridSize + spacingPx)
|
||||||
|
|
||||||
|
// Position the horizontal clues
|
||||||
|
val horizontalCluesOffsetY = gridSize + 2 * spacingPx
|
||||||
|
|
||||||
|
horizontalCluesBoxPlaceable.place(0, horizontalCluesOffsetY)
|
||||||
|
var offsetY = placeClues(
|
||||||
|
placeables = horizontalCluesPlaceables,
|
||||||
|
offsetX = 0,
|
||||||
|
offsetY = horizontalCluesOffsetY,
|
||||||
|
maxWidth = gridSize
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 time
|
||||||
|
val remainingSpace = constraints.maxHeight - offsetY
|
||||||
|
if (remainingSpace < timePlaceable.height) {
|
||||||
|
val scale = remainingSpace.toFloat() / timePlaceable.height.toFloat()
|
||||||
|
if (scale > 0.1f) {
|
||||||
|
timePlaceable.placeWithLayer(
|
||||||
|
x = constraints.maxWidth - timePlaceable.width - spacingPx,
|
||||||
|
y = constraints.maxHeight - timePlaceable.height
|
||||||
|
) {
|
||||||
|
scaleX = scale
|
||||||
|
scaleY = scale
|
||||||
|
translationX = (timePlaceable.width * (1 - scale)) / 2f
|
||||||
|
translationY = (timePlaceable.height * (1 - scale)) / 2f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
timePlaceable.place(
|
||||||
|
x = constraints.maxWidth - timePlaceable.width - spacingPx,
|
||||||
|
y = constraints.maxHeight - timePlaceable.height
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package ch.dissem.yaep.ui.common.layout
|
||||||
|
|
||||||
|
import androidx.compose.ui.layout.Measurable
|
||||||
|
import androidx.compose.ui.layout.Placeable
|
||||||
|
import androidx.compose.ui.unit.Constraints
|
||||||
|
import androidx.compose.ui.unit.Constraints.Companion.fixed
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
internal fun Placeable.PlacementScope.squarish(
|
||||||
|
constraints: Constraints,
|
||||||
|
spacingPx: Int,
|
||||||
|
grid: FocusGroup,
|
||||||
|
horizontalClues: FocusGroup,
|
||||||
|
verticalClues: FocusGroup,
|
||||||
|
timeMeasurable: Measurable
|
||||||
|
) {
|
||||||
|
val gridSize = (7 * min(constraints.maxWidth, constraints.maxHeight)) / 10
|
||||||
|
val gridItemSize = (gridSize - 12 * spacingPx) / 18
|
||||||
|
val rightBarWidth = constraints.maxWidth - gridSize - spacingPx
|
||||||
|
|
||||||
|
val gridConstraints = fixed(gridSize, gridSize)
|
||||||
|
val horizontalCluesConstraints = fixed(
|
||||||
|
width = 3 * gridItemSize + 2 * spacingPx,
|
||||||
|
height = gridItemSize + 2 * spacingPx
|
||||||
|
)
|
||||||
|
val verticalCluesConstraints = fixed(
|
||||||
|
width = gridItemSize + 2 * spacingPx,
|
||||||
|
height = 3 * gridItemSize + 2 * spacingPx
|
||||||
|
)
|
||||||
|
val timeConstraints = Constraints()
|
||||||
|
|
||||||
|
|
||||||
|
val gridPlaceable = grid.item.measure(gridConstraints)
|
||||||
|
val gridBoxPlaceable = grid.box.measure(gridConstraints)
|
||||||
|
val horizontalCluesPlaceables = horizontalClues.items.map {
|
||||||
|
it.measure(horizontalCluesConstraints)
|
||||||
|
}
|
||||||
|
val horizontalCluesBoxPlaceable = horizontalClues.box
|
||||||
|
.measure(
|
||||||
|
cluesBoxConstraints(
|
||||||
|
width = rightBarWidth,
|
||||||
|
itemConstraints = horizontalCluesConstraints,
|
||||||
|
itemCount = horizontalClues.count
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val verticalCluesPlaceables = verticalClues.items.map {
|
||||||
|
it.measure(verticalCluesConstraints)
|
||||||
|
}
|
||||||
|
val verticalCluesBoxPlaceable = verticalClues.box
|
||||||
|
.measure(
|
||||||
|
cluesBoxConstraints(
|
||||||
|
width = rightBarWidth,
|
||||||
|
itemConstraints = verticalCluesConstraints,
|
||||||
|
itemCount = verticalClues.count
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val timePlaceable = timeMeasurable.measure(timeConstraints)
|
||||||
|
|
||||||
|
// Position the grid
|
||||||
|
gridBoxPlaceable.place(0, 0)
|
||||||
|
gridPlaceable.place(0, 0)
|
||||||
|
|
||||||
|
// Position the horizontal clues
|
||||||
|
val horizontalCluesOffsetX = gridSize + 2 * spacingPx
|
||||||
|
horizontalCluesBoxPlaceable.place(horizontalCluesOffsetX, 0)
|
||||||
|
placeClues(
|
||||||
|
placeables = horizontalCluesPlaceables,
|
||||||
|
offsetX = horizontalCluesOffsetX,
|
||||||
|
offsetY = 0,
|
||||||
|
maxWidth = rightBarWidth
|
||||||
|
)
|
||||||
|
|
||||||
|
// Position the vertical clues
|
||||||
|
val verticalCluesOffsetY = gridSize + 2 * spacingPx
|
||||||
|
verticalCluesBoxPlaceable.place(0, verticalCluesOffsetY)
|
||||||
|
placeClues(
|
||||||
|
placeables = verticalCluesPlaceables,
|
||||||
|
offsetX = 0,
|
||||||
|
offsetY = verticalCluesOffsetY,
|
||||||
|
maxWidth = gridSize
|
||||||
|
)
|
||||||
|
|
||||||
|
// Position the time
|
||||||
|
timePlaceable.place(
|
||||||
|
x = constraints.maxWidth - timePlaceable.width - spacingPx,
|
||||||
|
y = constraints.maxHeight - timePlaceable.height
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user