diff --git a/app/composeApp/src/commonMain/kotlin/kr/tkrmagid/tetrig/ui/GameScreen.kt b/app/composeApp/src/commonMain/kotlin/kr/tkrmagid/tetrig/ui/GameScreen.kt index 2d75c14..fafd73d 100644 --- a/app/composeApp/src/commonMain/kotlin/kr/tkrmagid/tetrig/ui/GameScreen.kt +++ b/app/composeApp/src/commonMain/kotlin/kr/tkrmagid/tetrig/ui/GameScreen.kt @@ -181,14 +181,33 @@ private fun BoardWithPanels(state: GameState, ghostOn: Boolean, l: Layout, stack BoardCanvas(state, ghostOn) } Column(Modifier.width(panelW)) { - SidePanel("HOLD", listOfNotNull(state.game.hold), s, 1, l.isTiny) - Spacer(Modifier.height((8 * s).dp)) - SidePanel("NEXT", state.game.peekNext(if (l.isTiny) 3 else 5), s, if (l.isTiny) 3 else 5, l.isTiny) + SidePanels(state, s, l.isTiny) } } } } +/** + * HOLD / NEXT 패널. + * + * `game.hold` 와 `game.peekNext()` 는 Compose 가 관찰하지 못하는 평범한 엔진 + * 필드라, 값이 바뀌어도 그것만으로는 재구성이 일어나지 않는다. BoardCanvas 와 + * 똑같이 여기서 `state.version` 을 읽어 구독해야 한다. 이 구독이 없으면 게임이 + * 시작될 때의 넥스트 큐가 끝까지 그대로 멈춰 있는다. + * + * 보드 전체가 아니라 패널만 따로 떼어 둔 이유는, 재구성 범위를 이 서브트리로 + * 좁혀 매 프레임 BoxWithConstraints 측정까지 다시 돌지 않게 하기 위해서다. + */ +@Composable +private fun SidePanels(state: GameState, s: Float, tiny: Boolean) { + @Suppress("UNUSED_EXPRESSION") + state.version + val nextCount = if (tiny) 3 else 5 + SidePanel("HOLD", listOfNotNull(state.game.hold), s, 1, tiny) + Spacer(Modifier.height((8 * s).dp)) + SidePanel("NEXT", state.game.peekNext(nextCount), s, nextCount, tiny) +} + @Composable private fun BoardCanvas(state: GameState, ghostOn: Boolean) { @Suppress("UNUSED_EXPRESSION") @@ -307,16 +326,17 @@ private fun ControlPad(state: GameState, lefty: Boolean, scale: Float, compactHe horizontalArrangement = Arrangement.spacedBy((8 * scale).dp), ) { val actions: @Composable (Modifier) -> Unit = { m -> + // 회전·홀드는 위쪽 한 줄로 올리고, 하드 드롭은 맞은편 소프트 드롭과 + // 같은 자리(맨 아래 가로로 긴 낮은 버튼)에 둔다. 두 컬럼의 행 높이 + // 비율(1.4 : 1)을 맞춰야 좌우가 나란히 떨어진다. Column(m.fillMaxHeight(), verticalArrangement = Arrangement.spacedBy((8 * scale).dp)) { - Row(Modifier.weight(1f).fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy((8 * scale).dp)) { - CtrlButton("HOLD", Modifier.weight(2f).fillMaxHeight(), scale) { state.hold() } - CtrlButton("180°", Modifier.weight(1f).fillMaxHeight(), scale) { state.rotate("180") } - } - Row(Modifier.weight(1f).fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy((8 * scale).dp)) { + Row(Modifier.weight(1.4f).fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy((8 * scale).dp)) { CtrlButton("↺", Modifier.weight(1f).fillMaxHeight(), scale) { state.rotate("ccw") } CtrlButton("↻", Modifier.weight(1f).fillMaxHeight(), scale) { state.rotate("cw") } - CtrlButton("하드\n드롭", Modifier.weight(1f).fillMaxHeight(), scale, accent = true) { state.hardDrop() } + CtrlButton("180°", Modifier.weight(1f).fillMaxHeight(), scale) { state.rotate("180") } + CtrlButton("HOLD", Modifier.weight(1f).fillMaxHeight(), scale) { state.hold() } } + CtrlButton("하드 드롭", Modifier.weight(1f).fillMaxWidth(), scale, accent = true) { state.hardDrop() } } } val moves: @Composable (Modifier) -> Unit = { m -> @@ -328,11 +348,13 @@ private fun ControlPad(state: GameState, lefty: Boolean, scale: Float, compactHe HoldCtrl("▼ 소프트", Modifier.weight(1f).fillMaxWidth(), scale, { state.setSoft(true) }, { state.setSoft(false) }) } } + // 액션쪽이 한 줄에 버튼 4개라 폭을 더 준다. 1.1 이면 버튼이 세로로 길쭉해져 + // 엄지로 누르기 불편했다. if (lefty) { moves(Modifier.weight(1f)) - actions(Modifier.weight(1.1f)) + actions(Modifier.weight(1.35f)) } else { - actions(Modifier.weight(1.1f)) + actions(Modifier.weight(1.35f)) moves(Modifier.weight(1f)) } } diff --git a/store/shots/03_game.png b/store/shots/03_game.png index b0b5a3b..80aceab 100644 Binary files a/store/shots/03_game.png and b/store/shots/03_game.png differ diff --git a/store/shots/tablet_02_game.png b/store/shots/tablet_02_game.png index 5227ecb..02d8abd 100644 Binary files a/store/shots/tablet_02_game.png and b/store/shots/tablet_02_game.png differ diff --git a/store/shots/tablet_03_landscape.png b/store/shots/tablet_03_landscape.png index 7d60194..e838516 100644 Binary files a/store/shots/tablet_03_landscape.png and b/store/shots/tablet_03_landscape.png differ