diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 0bd91d4..0cc425a 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -34,6 +34,7 @@ kotlin { // injection implementation(libs.koin.compose) implementation(libs.koin.compose.viewmodel) + implementation(libs.engawapg.zoomable) // composable component. implementation(libs.coil.compose) implementation(libs.coil.network.ktor) diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_center_focus_weak_24dp.xml b/composeApp/src/commonMain/composeResources/drawable/ic_center_focus_weak_24dp.xml new file mode 100644 index 0000000..a92214c --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_center_focus_weak_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_zoom_in_map_24dp.xml b/composeApp/src/commonMain/composeResources/drawable/ic_zoom_in_map_24dp.xml new file mode 100644 index 0000000..144dc6c --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_zoom_in_map_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/ic_zoom_out_map_24dp.xml b/composeApp/src/commonMain/composeResources/drawable/ic_zoom_out_map_24dp.xml new file mode 100644 index 0000000..6cdc80e --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/ic_zoom_out_map_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/composeApp/src/commonMain/composeResources/drawable/image_dahome_maps.webp b/composeApp/src/commonMain/composeResources/drawable/image_dahome_maps.webp new file mode 100644 index 0000000..28f7f73 Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/image_dahome_maps.webp differ diff --git a/composeApp/src/commonMain/composeResources/drawable/image_dahome_regions.webp b/composeApp/src/commonMain/composeResources/drawable/image_dahome_regions.webp new file mode 100644 index 0000000..4927211 Binary files /dev/null and b/composeApp/src/commonMain/composeResources/drawable/image_dahome_regions.webp differ diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Camera.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Camera.kt new file mode 100644 index 0000000..c9eae2f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Camera.kt @@ -0,0 +1,101 @@ +package com.pixelized.desktop.lwa.ui.composable.scene + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.VectorConverter +import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.util.fastRoundToInt + +@Stable +class Camera( + private val initialZoom: Float = 2f, + private val initialOffset: IntOffset = IntOffset.Zero, +) { + private var _zoom = Animatable( + initialValue = initialZoom, + typeConverter = Float.VectorConverter, + ) + val zoom: Float get() = _zoom.value + + private var _offset = Animatable( + initialValue = initialOffset, + typeConverter = IntOffset.VectorConverter, + ) + val offset: IntOffset by derivedStateOf { + _offset.value + IntOffset( + x = (_sceneSize.width - cameraSizeZoomed.width) / 2, + y = (_sceneSize.height - cameraSizeZoomed.height) / 2, + ) + } + + private var _sceneSize: IntSize by mutableStateOf(IntSize.Zero) + private var _cameraSize: IntSize by mutableStateOf(IntSize.Zero) + val cameraSize: IntSize get() = _cameraSize + val cameraSizeZoomed: IntSize by derivedStateOf { + IntSize( + width = (cameraSize.width * zoom).fastRoundToInt(), + height = (cameraSize.height * zoom).fastRoundToInt(), + ) + } + + fun changeSizes( + sceneSize: IntSize, + cameraSize: IntSize, + ) { + _cameraSize = cameraSize + _sceneSize = sceneSize + } + + suspend fun handlePanning( + delta: Offset, + snap: Boolean, + ) { + val value = _offset.value - IntOffset( + x = (delta.x * zoom).fastRoundToInt(), + y = (delta.y * zoom).fastRoundToInt(), + ) + when { + snap -> _offset.snapTo(targetValue = value) + else -> _offset.animateTo(targetValue = value) + } + } + + suspend fun handleZoom( + zoomIn: Boolean, + power: Float, + snap: Boolean = false, + ) { + val value = _zoom.value * when { + zoomIn -> 1f - power + else -> 1f + power + } + when { + snap -> _zoom.snapTo(targetValue = value) + else -> _zoom.animateTo(targetValue = value) + } + } + + suspend fun resetPosition( + snap: Boolean = false, + ) { + when (snap) { + true -> _offset.snapTo(targetValue = initialOffset) + else -> _offset.animateTo(targetValue = initialOffset) + } + } + + suspend fun resetZoom( + snap: Boolean = false, + ) { + when (snap) { + true -> _zoom.snapTo(targetValue = initialZoom) + else -> _zoom.animateTo(targetValue = initialZoom) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/FogOfWar.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/FogOfWar.kt new file mode 100644 index 0000000..31e9e09 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/FogOfWar.kt @@ -0,0 +1,9 @@ +package com.pixelized.desktop.lwa.ui.composable.scene + +import androidx.compose.runtime.Stable +import androidx.compose.ui.graphics.Color + +@Stable +data class FogOfWar( + val color: Color = Color.Black.copy(alpha = 0.5f), +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Layout.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Layout.kt new file mode 100644 index 0000000..e211690 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Layout.kt @@ -0,0 +1,41 @@ +package com.pixelized.desktop.lwa.ui.composable.scene + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.VectorConverter +import androidx.compose.runtime.Stable +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize + +@Stable +class Layout( + val texture: ImageBitmap, + val offset: IntOffset = IntOffset.Zero, + val size: IntSize = IntSize(texture.width, texture.height), + private val initialAlpha: Float = 1f, +) { + private val _alpha = Animatable( + initialValue = initialAlpha, + typeConverter = Float.VectorConverter, + ) + val alpha get() = _alpha.value + + suspend fun alpha( + alpha: Float, + snap: Boolean = false, + ) { + when (snap) { + true -> _alpha.snapTo(targetValue = alpha) + else -> _alpha.animateTo(targetValue = alpha) + } + } + + suspend fun resetAlpha( + snap: Boolean = false, + ) { + when (snap) { + true -> _alpha.snapTo(targetValue = initialAlpha) + else -> _alpha.animateTo(targetValue = initialAlpha) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Scene.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Scene.kt new file mode 100644 index 0000000..9e2ed21 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Scene.kt @@ -0,0 +1,278 @@ +package com.pixelized.desktop.lwa.ui.composable.scene + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.isAltPressed +import androidx.compose.ui.input.pointer.isCtrlPressed +import androidx.compose.ui.input.pointer.isPrimaryPressed +import androidx.compose.ui.input.pointer.isTertiaryPressed +import androidx.compose.ui.input.pointer.onPointerEvent +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.screen.campaign.LocalCampaignLayoutScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.ic_center_focus_weak_24dp +import lwacharactersheet.composeapp.generated.resources.ic_zoom_in_map_24dp +import lwacharactersheet.composeapp.generated.resources.ic_zoom_out_map_24dp +import lwacharactersheet.composeapp.generated.resources.ic_visibility_24dp +import lwacharactersheet.composeapp.generated.resources.icon_d100 +import lwacharactersheet.composeapp.generated.resources.image_dahome_maps +import lwacharactersheet.composeapp.generated.resources.image_dahome_regions +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.imageResource +import org.jetbrains.compose.resources.painterResource +import kotlin.math.sign + +@Stable +data class Scene( + val camera: Camera, + val layouts: List, + val fogOfWar: FogOfWar, +) { + val size: IntSize = IntSize( + width = layouts.maxOf { it.size.width }, + height = layouts.maxOf { it.size.height }, + ) +} + +@Composable +fun Scene( + modifier: Modifier, +) { + val campaign = LocalCampaignLayoutScope.current + val scope = rememberCoroutineScope() + val scene = rememberScene( + camera = Camera( + initialZoom = 1f, + initialOffset = IntOffset(x = -150, y = -120), + ), + fogOfWar = FogOfWar(), + rememberLayoutFromResource( + resource = Res.drawable.image_dahome_maps, + ), + rememberLayoutFromResource( + resource = Res.drawable.image_dahome_regions, + ), + rememberLayoutFromResource( + resource = Res.drawable.icon_d100, + offset = IntOffset(x = 1740, y = 910), + ), + ) + Box( + modifier = modifier + .graphicsLayer { clip = true } + .onCameraControl(scope = scope, scene = scene) + .drawScene(scene = scene) + .fogOfWar(scene = scene) + ) { + Column( + modifier = Modifier + .align(alignment = Alignment.BottomEnd) + .padding(end = campaign.rightPanel.value.width) + .padding(all = 8.dp) + ) { + IconButton( + onClick = { + scope.launch { + scene.camera.handleZoom( + zoomIn = true, + power = 0.3f, + ) + } + } + ) { + Icon( + painter = painterResource(Res.drawable.ic_zoom_in_map_24dp), + contentDescription = null + ) + } + IconButton( + onClick = { + scope.launch { + scene.camera.handleZoom( + zoomIn = false, + power = 0.3f, + ) + } + } + ) { + Icon( + painter = painterResource(Res.drawable.ic_zoom_out_map_24dp), + contentDescription = null + ) + } + IconButton( + onClick = { + scope.launch { + scene.camera.resetPosition() + } + scope.launch { + scene.camera.resetZoom() + } + } + ) { + Icon( + painter = painterResource(Res.drawable.ic_center_focus_weak_24dp), + contentDescription = null + ) + } + IconButton( + onClick = { + scope.launch { + scene.layouts.getOrNull(1)?.let { + it.alpha(alpha = if (it.alpha == 0f) 1f else 0f) + } + } + } + ) { + Icon( + painter = painterResource(Res.drawable.ic_visibility_24dp), + contentDescription = null + ) + } + } + } +} + +@Composable +@Stable +fun rememberLayoutFromResource( + resource: DrawableResource, + offset: IntOffset = IntOffset.Zero, +): Layout { + val texture = imageResource( + resource = resource, + ) + return remember(resource) { + Layout( + texture = texture, + offset = offset, + ) + } +} + +@Composable +@Stable +fun rememberScene( + camera: Camera, + fogOfWar: FogOfWar, + vararg layouts: Layout, +): Scene { + return remember { + Scene( + camera = camera, + layouts = layouts.toList(), + fogOfWar = fogOfWar, + ) + } +} + +@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class) +fun Modifier.onCameraControl( + scope: CoroutineScope, + scene: Scene, +): Modifier { + val offsetDelta = CursorDelta() + return this + .onSizeChanged { + scene.camera.changeSizes( + sceneSize = scene.size, + cameraSize = it, + ) + } + .onPointerEvent(PointerEventType.Move) { event: PointerEvent -> + scope.launch { + offsetDelta.handlePositionChange( + event = event, + ) { delta -> + when { + event.buttons.isTertiaryPressed || (event.keyboardModifiers.isCtrlPressed && event.buttons.isPrimaryPressed) -> scene.camera.handlePanning( + delta = delta, + snap = true, + ) + + event.keyboardModifiers.isAltPressed -> scene.camera.handleZoom( + zoomIn = delta.y.sign < 0f, + power = 0.025f, + snap = true, + ) + } + } + } + } + .onPointerEvent(PointerEventType.Scroll) { event: PointerEvent -> + scope.launch { + scene.camera.handleZoom( + zoomIn = event.changes.first().scrollDelta.y.sign < 0f, + power = 0.15f, + snap = false, + ) + } + } +} + +fun Modifier.drawScene( + scene: Scene, +): Modifier = this.drawWithCache { + onDrawBehind { + scene.layouts.forEach { layout -> + drawImage( + image = layout.texture, + srcOffset = scene.camera.offset - layout.offset, + srcSize = scene.camera.cameraSizeZoomed, + dstSize = scene.camera.cameraSize, + alpha = layout.alpha, + ) + } + } +} + +fun Modifier.fogOfWar( + scene: Scene, +): Modifier = this.drawWithCache { + onDrawBehind { + drawRect(color = scene.fogOfWar.color) + } +} + +private data class CursorDelta( + var lastDeltaTimestamp: Long = System.currentTimeMillis(), + var previousPosition: Offset = Offset.Zero, + var currentPosition: Offset = Offset.Zero, +) { + suspend inline fun handlePositionChange( + event: PointerEvent, + delay: Float = 10f, + crossinline block: suspend (delta: Offset) -> Unit, + ) { + val currentTimestamp = System.currentTimeMillis() + if (currentTimestamp - lastDeltaTimestamp > delay) { + lastDeltaTimestamp = currentTimestamp + previousPosition = currentPosition + currentPosition = event.changes.first().position + block(currentPosition - previousPosition) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt index c18e99b..e43f9c7 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt @@ -1,11 +1,13 @@ package com.pixelized.desktop.lwa.ui.screen.campaign +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -18,7 +20,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.isAltPressed import androidx.compose.ui.input.key.isCtrlPressed @@ -41,6 +45,7 @@ import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterShe import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterSheetDiminishedDialogViewModel import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler +import com.pixelized.desktop.lwa.ui.composable.scene.Scene import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToLevelScreen import com.pixelized.desktop.lwa.ui.overlay.portrait.PortraitOverlay @@ -55,6 +60,7 @@ import com.pixelized.desktop.lwa.ui.screen.campaign.text.CampaignChat import com.pixelized.desktop.lwa.ui.screen.campaign.text.CampaignChatViewModel import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbar import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbarViewModel +import com.pixelized.desktop.lwa.ui.theme.lwa import kotlinx.coroutines.launch import org.koin.compose.viewmodel.koinViewModel @@ -95,7 +101,9 @@ fun CampaignScreen( }, main = { - + Scene( + modifier = Modifier.matchParentSize(), + ) }, chat = { CampaignChat( @@ -115,7 +123,8 @@ fun CampaignScreen( }, leftPanel = { PlayerRibbon( - modifier = Modifier.fillMaxHeight(), + modifier = Modifier.fillMaxHeight() + .background(color = Color.Black.copy(alpha = 0.5f)), viewModel = playerRibbonViewModel, onCharacterLeftClick = { scope.launch { @@ -140,7 +149,8 @@ fun CampaignScreen( }, rightPanel = { NpcRibbon( - modifier = Modifier.fillMaxHeight(), + modifier = Modifier.fillMaxHeight() + .background(color = Color.Black.copy(alpha = 0.5f)), onCharacterLeftClick = { scope.launch { npcDetailViewModel.showCharacter( @@ -166,11 +176,12 @@ fun CampaignScreen( CharacterDetailPanel( modifier = Modifier .padding(all = 8.dp) + .padding(start = MaterialTheme.lwa.size.portrait.minimized.width * 2 + 20.dp) .width(width = 128.dp * 4) .fillMaxHeight(), transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Ltr), blurController = blurController, - detailPanelViewModel = npcDetailViewModel, + detailPanelViewModel = playerDetailViewModel, characterDiminishedViewModel = dismissedViewModel, characteristicDialogViewModel = characteristicDialogViewModel, alterationViewModel = alterationViewModel, @@ -183,11 +194,12 @@ fun CampaignScreen( CharacterDetailPanel( modifier = Modifier .padding(all = 8.dp) + .padding(end = MaterialTheme.lwa.size.portrait.minimized.width * 2 + 20.dp) .width(width = 128.dp * 4) .fillMaxHeight(), transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Rtl), blurController = blurController, - detailPanelViewModel = playerDetailViewModel, + detailPanelViewModel = npcDetailViewModel, characterDiminishedViewModel = dismissedViewModel, characteristicDialogViewModel = characteristicDialogViewModel, alterationViewModel = alterationViewModel, @@ -308,6 +320,7 @@ private fun CampaignLayout( ) { val density = LocalDensity.current + val mainState = remember { mutableStateOf(DpSize.Unspecified) } val leftOverlayState = remember { mutableStateOf(DpSize.Unspecified) } val leftPanelState = remember { mutableStateOf(DpSize.Unspecified) } val rightOverlayState = remember { mutableStateOf(DpSize.Unspecified) } @@ -315,6 +328,7 @@ private fun CampaignLayout( val chatOverlayState = remember { mutableStateOf(DpSize.Unspecified) } val scope = remember { CampaignLayoutScope( + main = mainState, leftOverlay = leftOverlayState, leftPanel = leftPanelState, rightOverlay = rightOverlayState, @@ -335,14 +349,18 @@ private fun CampaignLayout( ) { Box( modifier = Modifier - .align(alignment = Alignment.Center) - .fillMaxSize(), + .onSizeChanged { mainState.value = it.toDp(density) } + .matchParentSize(), ) { main() } Box( modifier = Modifier - .align(alignment = Alignment.BottomEnd) + .align(alignment = Alignment.BottomStart) + .padding( + start = MaterialTheme.lwa.size.portrait.minimized.width * 2 + 20.dp, + end = MaterialTheme.lwa.size.portrait.minimized.width * 2 + 20.dp + 56.dp, + ) .onSizeChanged { chatOverlayState.value = it.toDp(density) }, ) { chat() @@ -394,24 +412,31 @@ private fun CampaignKeyHandler( onPlayerNumber: (index: Int) -> Unit, onAltPLayerNumber: (index: Int) -> Unit, ) { + fun KeyEvent.callback(index: Int) { + if (isAltPressed) onAltPLayerNumber(index) else onPlayerNumber(index) + } KeyHandler { - if (it.type != KeyEventType.KeyDown) return@KeyHandler false + if (it.type != KeyEventType.KeyDown) { + return@KeyHandler false + } if (it.key == Key.Escape) { onDismissRequest() return@KeyHandler true } - if (it.isCtrlPressed.not()) return@KeyHandler false + if (it.isCtrlPressed.not()) { + return@KeyHandler false + } when (it.key) { Key.Escape -> onDismissRequest() - Key.One, Key.NumPad1 -> if (it.isAltPressed) onAltPLayerNumber(0) else onPlayerNumber(0) - Key.Two, Key.NumPad2 -> if (it.isAltPressed) onAltPLayerNumber(1) else onPlayerNumber(1) - Key.Three, Key.NumPad3 -> if (it.isAltPressed) onAltPLayerNumber(2) else onPlayerNumber(2) - Key.Four, Key.NumPad4 -> if (it.isAltPressed) onAltPLayerNumber(3) else onPlayerNumber(3) - Key.Five, Key.NumPad5 -> if (it.isAltPressed) onAltPLayerNumber(4) else onPlayerNumber(4) - Key.Six, Key.NumPad6 -> if (it.isAltPressed) onAltPLayerNumber(5) else onPlayerNumber(5) - Key.Seven, Key.NumPad7 -> if (it.isAltPressed) onAltPLayerNumber(6) else onPlayerNumber(6) - Key.Eight, Key.NumPad8 -> if (it.isAltPressed) onAltPLayerNumber(7) else onPlayerNumber(7) - Key.Nine, Key.NumPad9 -> if (it.isAltPressed) onAltPLayerNumber(8) else onPlayerNumber(8) + Key.One, Key.NumPad1 -> it.callback(index = 0) + Key.Two, Key.NumPad2 -> it.callback(index = 1) + Key.Three, Key.NumPad3 -> it.callback(index = 2) + Key.Four, Key.NumPad4 -> it.callback(index = 3) + Key.Five, Key.NumPad5 -> it.callback(index = 4) + Key.Six, Key.NumPad6 -> it.callback(index = 5) + Key.Seven, Key.NumPad7 -> it.callback(index = 6) + Key.Eight, Key.NumPad8 -> it.callback(index = 7) + Key.Nine, Key.NumPad9 -> it.callback(index = 8) else -> return@KeyHandler false } return@KeyHandler true @@ -427,6 +452,7 @@ private fun IntSize.toDp(density: Density) = with(density) { @Stable data class CampaignLayoutScope( + val main: State, val leftOverlay: State, val leftPanel: State, val rightOverlay: State, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6d32363..fb24483 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,7 @@ koin = "4.0.0" turtle = "0.10.0" logback = "1.5.17" coil = "3.1.0" +zoomable = "2.7.0" ui-graphics-android = "1.7.8" buildkonfig = "0.17.0" @@ -35,6 +36,7 @@ kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx- # UI. coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" } +engawapg-zoomable = { module = "net.engawapg.lib:zoomable", version.ref = "zoomable" } # Injection with Koin koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-ktor = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }