Intial scene commit

This commit is contained in:
Thomas Andres Gomez 2025-05-10 14:32:29 +02:00
parent fc06e3ef95
commit 76fc199d5e
20 changed files with 583 additions and 233 deletions

View file

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

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M480,640Q414,640 367,593Q320,546 320,480Q320,414 367,367Q414,320 480,320Q546,320 593,367Q640,414 640,480Q640,546 593,593Q546,640 480,640ZM480,560Q513,560 536.5,536.5Q560,513 560,480Q560,447 536.5,423.5Q513,400 480,400Q447,400 423.5,423.5Q400,447 400,480Q400,513 423.5,536.5Q447,560 480,560ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM200,840Q167,840 143.5,816.5Q120,793 120,760L120,600L200,600L200,760Q200,760 200,760Q200,760 200,760L360,760L360,840L200,840ZM600,840L600,760L760,760Q760,760 760,760Q760,760 760,760L760,600L840,600L840,760Q840,793 816.5,816.5Q793,840 760,840L600,840ZM120,360L120,200Q120,167 143.5,143.5Q167,120 200,120L360,120L360,200L200,200Q200,200 200,200Q200,200 200,200L200,360L120,360ZM760,360L760,200Q760,200 760,200Q760,200 760,200L600,200L600,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,360L760,360Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M156,860L100,804L224,680L120,680L120,600L360,600L360,840L280,840L280,736L156,860ZM804,860L680,736L680,840L600,840L600,600L840,600L840,680L736,680L860,804L804,860ZM120,360L120,280L224,280L100,156L156,100L280,224L280,120L360,120L360,360L120,360ZM600,360L600,120L680,120L680,224L804,100L860,156L736,280L840,280L840,360L600,360Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M120,840L120,600L200,600L200,704L324,580L380,636L256,760L360,760L360,840L120,840ZM600,840L600,760L704,760L580,636L636,580L760,704L760,600L840,600L840,840L600,840ZM324,380L200,256L200,360L120,360L120,120L360,120L360,200L256,200L380,324L324,380ZM636,380L580,324L704,200L600,200L600,120L840,120L840,360L760,360L760,256L636,380Z" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

View file

@ -40,8 +40,8 @@ import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.Characte
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanelViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.header.CharacterDetailHeaderFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.inventory.CharacterDetailInventoryFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.npc.NpcRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player.PlayerRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.NpcRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.text.CampaignChatViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.text.TextMessageFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbarViewModel

View file

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

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@ import androidx.compose.foundation.TooltipPlacement
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
@ -56,7 +56,7 @@ fun BasicTooltipLayout(
tips = tooltip,
tooltip = {
BasicTooltip(
modifier = Modifier.width(width = 448.dp),
modifier = Modifier.widthIn(max = 448.dp),
elevation = elevation,
tooltip = it,
)
@ -72,7 +72,9 @@ private fun BasicTooltip(
tooltip: BasicTooltipUio,
) {
Surface(
modifier = Modifier.padding(16.dp).then(other = modifier),
modifier = Modifier
.padding(16.dp)
.then(other = modifier),
color = MaterialTheme.colors.surface,
elevation = elevation,
shape = remember { RoundedCornerShape(4.dp) }

View file

@ -2,11 +2,11 @@ package com.pixelized.desktop.lwa.ui.screen.campaign
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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
@ -20,6 +20,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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
@ -42,6 +43,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
@ -49,13 +51,14 @@ import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetai
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.CharacterDetailPanelViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.DetailPanelUio
import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.rememberTransitionAnimation
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.npc.NpcRibbon
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player.PlayerRibbon
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player.PlayerRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.CharacterRibbon
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.NpcRibbonViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.PlayerRibbonViewModel
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
@ -67,6 +70,7 @@ val LocalCampaignLayoutScope = compositionLocalOf<CampaignLayoutScope> {
fun CampaignScreen(
playerRibbonViewModel: PlayerRibbonViewModel = koinViewModel(),
playerDetailViewModel: CharacterDetailPanelViewModel = koinViewModel(key = "player"),
npcRibbonViewModel: NpcRibbonViewModel = koinViewModel(),
npcDetailViewModel: CharacterDetailPanelViewModel = koinViewModel(key = "npc"),
characteristicDialogViewModel: CharacterSheetCharacteristicDialogViewModel = koinViewModel(),
dismissedViewModel: CharacterSheetDiminishedDialogViewModel = koinViewModel(),
@ -96,7 +100,9 @@ fun CampaignScreen(
},
main = {
Scene(
modifier = Modifier.matchParentSize(),
)
},
chat = {
CampaignChat(
@ -115,9 +121,10 @@ fun CampaignScreen(
}
},
leftPanel = {
PlayerRibbon(
CharacterRibbon(
modifier = Modifier.fillMaxHeight(),
viewModel = playerRibbonViewModel,
layoutDirection = LayoutDirection.Ltr,
onCharacterLeftClick = {
scope.launch {
playerDetailViewModel.showCharacter(
@ -140,8 +147,10 @@ fun CampaignScreen(
)
},
rightPanel = {
NpcRibbon(
CharacterRibbon(
modifier = Modifier.fillMaxHeight(),
viewModel = npcRibbonViewModel,
layoutDirection = LayoutDirection.Rtl,
onCharacterLeftClick = {
scope.launch {
npcDetailViewModel.showCharacter(
@ -167,10 +176,11 @@ fun CampaignScreen(
CharacterDetailPanel(
modifier = Modifier
.padding(all = 8.dp)
.padding(start = MaterialTheme.lwa.size.portrait.minimized.width + 8.dp)
.width(width = 128.dp * 4)
.fillMaxHeight(),
transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Ltr),
detailPanelViewModel = npcDetailViewModel,
detailPanelViewModel = playerDetailViewModel,
characterDiminishedViewModel = dismissedViewModel,
characteristicDialogViewModel = characteristicDialogViewModel,
alterationViewModel = alterationViewModel,
@ -183,10 +193,11 @@ fun CampaignScreen(
CharacterDetailPanel(
modifier = Modifier
.padding(all = 8.dp)
.padding(end = MaterialTheme.lwa.size.portrait.minimized.width + 8.dp)
.width(width = 128.dp * 4)
.fillMaxHeight(),
transitionSpec = rememberTransitionAnimation(direction = LayoutDirection.Rtl),
detailPanelViewModel = playerDetailViewModel,
detailPanelViewModel = npcDetailViewModel,
characterDiminishedViewModel = dismissedViewModel,
characteristicDialogViewModel = characteristicDialogViewModel,
alterationViewModel = alterationViewModel,
@ -205,7 +216,7 @@ fun CampaignScreen(
characteristicDialogViewModel.changeSubCharacteristic(
characterSheetId = dialog.characterSheetId,
characteristic = dialog.characteristic,
useArmor= dialog.enableArmor?.checked?.value == true,
useArmor = dialog.enableArmor?.checked?.value == true,
value = dialog.value.valueFlow.value.toIntOrNull() ?: 0,
)
characteristicDialogViewModel.hideSubCharacteristicDialog()
@ -307,6 +318,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) }
@ -314,6 +326,7 @@ private fun CampaignLayout(
val chatOverlayState = remember { mutableStateOf(DpSize.Unspecified) }
val scope = remember {
CampaignLayoutScope(
main = mainState,
leftOverlay = leftOverlayState,
leftPanel = leftPanelState,
rightOverlay = rightOverlayState,
@ -334,30 +347,35 @@ private fun CampaignLayout(
) {
Box(
modifier = Modifier
.align(alignment = Alignment.Center)
.fillMaxSize(),
.onSizeChanged { mainState.value = it.toDp(density) }
.matchParentSize(),
) {
main()
}
Row {
Box(
modifier = Modifier.onSizeChanged { leftPanelState.value = it.toDp(density) },
) {
leftPanel()
}
Box(
modifier = Modifier
.align(alignment = Alignment.Bottom)
.weight(weight = 1f)
.onSizeChanged { chatOverlayState.value = it.toDp(density) },
) {
chat()
}
Box(
modifier = Modifier.onSizeChanged { rightPanelState.value = it.toDp(density) },
) {
rightPanel()
}
Box(
modifier = Modifier
.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()
}
Box(
modifier = Modifier
.align(alignment = Alignment.CenterStart)
.onSizeChanged { leftPanelState.value = it.toDp(density) },
) {
leftPanel()
}
Box(
modifier = Modifier
.align(alignment = Alignment.CenterEnd)
.onSizeChanged { rightPanelState.value = it.toDp(density) },
) {
rightPanel()
}
Box(
modifier = Modifier
@ -392,24 +410,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
@ -425,6 +450,7 @@ private fun IntSize.toDp(density: Density) = with(density) {
@Stable
data class CampaignLayoutScope(
val main: State<DpSize>,
val leftOverlay: State<DpSize>,
val leftPanel: State<DpSize>,
val rightOverlay: State<DpSize>,

View file

@ -17,14 +17,12 @@ import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonPortrait
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonRoll
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonAlteration
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player.PlayerRibbonViewModel
import org.koin.compose.viewmodel.koinViewModel
@Composable
fun CharacterRibbon(
modifier: Modifier = Modifier,
layoutDirection: LayoutDirection,
viewModel: PlayerRibbonViewModel = koinViewModel(),
viewModel: CharacterRibbonViewModel,
padding: PaddingValues = PaddingValues(all = 8.dp),
onCharacterLeftClick: (characterSheetId: String) -> Unit,
onCharacterRightClick: (characterSheetId: String) -> Unit,

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.npc
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
@ -6,8 +6,6 @@ import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetReposit
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.repository.settings.model.Settings
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.CharacterRibbonFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.CharacterRibbonViewModel
import com.pixelized.shared.lwa.model.campaign.Campaign
class NpcRibbonViewModel(

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
@ -6,8 +6,6 @@ import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetReposit
import com.pixelized.desktop.lwa.repository.roll_history.RollHistoryRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import com.pixelized.desktop.lwa.repository.settings.model.Settings
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.CharacterRibbonFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.CharacterRibbonViewModel
import com.pixelized.shared.lwa.model.campaign.Campaign
class PlayerRibbonViewModel(

View file

@ -32,6 +32,7 @@ import com.pixelized.desktop.lwa.ui.theme.lwa
@Stable
data class CharacterRibbonAlterationUio(
val icon: String,
val tooltips: BasicTooltipUio?,
)
@ -44,60 +45,56 @@ fun CharacterRibbonAlteration(
direction: LayoutDirection,
status: List<List<CharacterRibbonAlterationUio>>,
) {
val currentDirection: LayoutDirection = LocalLayoutDirection.current
val currentDirection = LocalLayoutDirection.current
CompositionLocalProvider(
LocalLayoutDirection provides direction
Row(
modifier = Modifier
.animateContentSize()
.size(size = size)
.then(other = modifier),
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Row(
modifier = Modifier
.animateContentSize()
.size(size = size)
.then(other = modifier),
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
CompositionLocalProvider(
LocalLayoutDirection provides currentDirection
status.forEach { columns ->
Column(
modifier = Modifier.animateContentSize(),
verticalArrangement = Arrangement.spacedBy(space = 2.dp),
) {
status.forEach { columns ->
Column(
modifier = Modifier.animateContentSize(),
verticalArrangement = Arrangement.spacedBy(space = 2.dp),
) {
columns.forEach {
BasicTooltipLayout(
delayMillis = 0,
tooltip = it.tooltips,
tooltipPlacement = remember(currentDirection) {
TooltipPlacement.ComponentRect(
anchor = when (direction) {
LayoutDirection.Ltr -> Alignment.TopStart
LayoutDirection.Rtl -> Alignment.TopEnd
},
alignment = when (direction) {
LayoutDirection.Ltr -> Alignment.BottomEnd
LayoutDirection.Rtl -> Alignment.BottomStart
},
CompositionLocalProvider(
LocalLayoutDirection provides LayoutDirection.Ltr
) {
columns.forEach {
BasicTooltipLayout(
delayMillis = 0,
tooltip = it.tooltips,
tooltipPlacement = remember(currentDirection) {
TooltipPlacement.ComponentRect(
anchor = when (direction) {
LayoutDirection.Ltr -> Alignment.CenterEnd
LayoutDirection.Rtl -> Alignment.CenterStart
},
alignment = when (direction) {
LayoutDirection.Ltr -> Alignment.CenterEnd
LayoutDirection.Rtl -> Alignment.CenterStart
},
)
},
content = {
AnimatedContent(
targetState = it.icon,
transitionSpec = { fadeIn() togetherWith fadeOut() },
) { icon ->
AsyncImage(
modifier = Modifier.size(24.dp),
model = ImageRequest.Builder(context = PlatformContext.INSTANCE)
.data(data = icon)
.size(size = 48)
.build(),
filterQuality = FilterQuality.High,
contentDescription = null,
)
},
content = {
AnimatedContent(
targetState = it.icon,
transitionSpec = { fadeIn() togetherWith fadeOut() },
) { icon ->
AsyncImage(
modifier = Modifier.size(24.dp),
model = ImageRequest.Builder(context = PlatformContext.INSTANCE)
.data(data = icon)
.size(size = 48)
.build(),
filterQuality = FilterQuality.High,
contentDescription = null,
)
}
}
)
}
}
)
}
}
}

View file

@ -1,64 +0,0 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.npc
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonPortrait
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonRoll
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonAlteration
import org.koin.compose.viewmodel.koinViewModel
@Composable
fun NpcRibbon(
modifier: Modifier = Modifier,
viewModel: NpcRibbonViewModel = koinViewModel(),
padding: PaddingValues = PaddingValues(all = 8.dp),
onCharacterLeftClick: (characterSheetId: String) -> Unit,
onCharacterRightClick: (characterSheetId: String) -> Unit,
onLevelUp: (characterSheetId: String) -> Unit,
) {
val characters = viewModel.characters.collectAsState()
LazyColumn(
modifier = modifier,
contentPadding = padding,
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) {
items(
items = characters.value,
key = { it.characterSheetId },
) {
Row(
modifier = Modifier
.animateItem()
.graphicsLayer { if (it.hideOverruled) this.alpha = 0.3f },
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
CharacterRibbonAlteration(
status = it.status,
direction = LayoutDirection.Rtl,
)
Box {
CharacterRibbonPortrait(
character = it.portrait,
onCharacterLeftClick = { onCharacterLeftClick(it.characterSheetId) },
onCharacterRightClick = { onCharacterRightClick(it.characterSheetId) },
onLevelUp = { onLevelUp(it.characterSheetId) },
)
CharacterRibbonRoll(
value = viewModel.roll(characterSheetId = it.characterSheetId).value,
)
}
}
}
}
}

View file

@ -1,64 +0,0 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.player
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonPortrait
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonRoll
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.common.CharacterRibbonAlteration
import org.koin.compose.viewmodel.koinViewModel
@Composable
fun PlayerRibbon(
modifier: Modifier = Modifier,
viewModel: PlayerRibbonViewModel = koinViewModel(),
padding: PaddingValues = PaddingValues(all = 8.dp),
onCharacterLeftClick: (characterSheetId: String) -> Unit,
onCharacterRightClick: (characterSheetId: String) -> Unit,
onLevelUp: (characterSheetId: String) -> Unit,
) {
val characters = viewModel.characters.collectAsState()
LazyColumn(
modifier = modifier,
contentPadding = padding,
verticalArrangement = Arrangement.spacedBy(space = 8.dp)
) {
items(
items = characters.value,
key = { it.characterSheetId },
) {
Row(
modifier = Modifier
.animateItem()
.graphicsLayer { if (it.hideOverruled) this.alpha = 0.3f },
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Box {
CharacterRibbonPortrait(
character = it.portrait,
onCharacterLeftClick = { onCharacterLeftClick(it.characterSheetId) },
onCharacterRightClick = { onCharacterRightClick(it.characterSheetId) },
onLevelUp = { onLevelUp(it.characterSheetId) },
)
CharacterRibbonRoll(
value = viewModel.roll(characterSheetId = it.characterSheetId).value,
)
}
CharacterRibbonAlteration(
status = it.status,
direction = LayoutDirection.Ltr,
)
}
}
}
}

View file

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