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