Refactor adaptive layout

This commit is contained in:
2025-12-25 11:20:11 +01:00
committed by Christian Basler
parent 25d4da4582
commit 3582196720
9 changed files with 539 additions and 482 deletions

View File

@@ -15,7 +15,9 @@ 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 ch.dissem.yaep.ui.common.focus.FocusFollowingFocusable
import ch.dissem.yaep.ui.common.focus.SelectionManager
import ch.dissem.yaep.ui.common.layout.AdaptiveGameLayout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
@@ -26,7 +28,7 @@ import kotlin.time.ExperimentalTime
@OptIn(ExperimentalTime::class)
fun App(
modifier: Modifier = Modifier,
rootSelectionManager: SelectionManager<*>,
rootSelectionManager: SelectionManager<FocusFollowingFocusable>,
spacing: Dp,
game: Game,
onNewGame: () -> Unit,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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