Add MapView into the App

This commit is contained in:
Thomas Andres Gomez 2025-11-26 18:03:09 +01:00
parent 3d5f29c18c
commit d648b8a05e
36 changed files with 867 additions and 745 deletions

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,760Q546,760 593,713Q640,666 640,600L640,440Q640,374 593,327Q546,280 480,280Q414,280 367,327Q320,374 320,440L320,600Q320,666 367,713Q414,760 480,760ZM400,640L560,640L560,560L400,560L400,640ZM400,480L560,480L560,400L400,400L400,480ZM480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520ZM480,840Q415,840 359.5,808Q304,776 272,720L160,720L160,640L244,640Q241,620 240.5,600Q240,580 240,560L160,560L160,480L240,480Q240,460 240.5,440Q241,420 244,400L160,400L160,320L272,320Q286,297 303.5,277Q321,257 344,242L280,176L336,120L422,206Q450,197 479,197Q508,197 536,206L624,120L680,176L614,242Q637,257 655.5,276.5Q674,296 688,320L800,320L800,400L716,400Q719,420 719.5,440Q720,460 720,480L800,480L800,560L720,560Q720,580 719.5,600Q719,620 716,640L800,640L800,720L688,720Q656,776 600.5,808Q545,840 480,840ZM40,240L40,120Q40,87 63.5,63.5Q87,40 120,40L240,40L240,120L120,120Q120,120 120,120Q120,120 120,120L120,240L40,240ZM240,920L120,920Q87,920 63.5,896.5Q40,873 40,840L40,720L120,720L120,840Q120,840 120,840Q120,840 120,840L240,840L240,920ZM720,920L720,840L840,840Q840,840 840,840Q840,840 840,840L840,720L920,720L920,840Q920,873 896.5,896.5Q873,920 840,920L720,920ZM840,240L840,120Q840,120 840,120Q840,120 840,120L720,120L720,40L840,40Q873,40 896.5,63.5Q920,87 920,120L920,240L840,240Z" />
</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="M600,840L360,756L174,828Q154,836 137,823.5Q120,811 120,790L120,230Q120,217 127.5,207Q135,197 148,192L360,120L600,204L786,132Q806,124 823,136.5Q840,149 840,170L840,730Q840,743 832.5,753Q825,763 812,768L600,840ZM560,742L560,274L400,218L400,686L560,742ZM640,742L760,702L760,228L640,274L640,742ZM200,732L320,686L320,218L200,258L200,732ZM640,274L640,274L640,742L640,742L640,274ZM320,218L320,218L320,686L320,686L320,218Z" />
</vector>

View file

@ -41,8 +41,8 @@ import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.sheet.Characte
import com.pixelized.desktop.lwa.ui.screen.campaign.player.ribbon.CharacterRibbonFactory
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.chatbox.CampaignChatViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.TextMessageFactory
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.CampaignToolbarViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.links.ResourcesViewModel
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.network.NetworkFactory

View file

@ -1,177 +0,0 @@
package com.pixelized.desktop.lwa.ui.composable.scene
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.VectorConverter
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastRoundToInt
import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneLayer
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography
@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)
}
}
}
@Composable
fun SceneCameraDebug(
modifier: Modifier = Modifier,
camera: Camera,
isOpen: Boolean = true,
style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug,
padding: Dp = MaterialTheme.lwa.dimen.debug.offset,
) {
val isOpen = remember { mutableStateOf(isOpen) }
Column(
modifier = Modifier
.clickable { isOpen.value = isOpen.value.not() }
.widthIn(min = MaterialTheme.lwa.dimen.debug.panelWidth)
.then(other = modifier),
verticalArrangement = Arrangement.spacedBy(space = 2.dp),
) {
Text(
style = MaterialTheme.lwa.typography.debug.title,
color = MaterialTheme.lwa.colorScheme.base.primary,
text = "Camera",
)
if (isOpen.value) {
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("offset: ") }
append(camera.offset.toString())
},
)
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("size: ") }
append(camera.cameraSize.toString())
},
)
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("zoom: ") }
append(camera.zoom.toString())
},
)
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("size: ") }
append(camera.cameraSizeZoomed.toString())
},
)
}
}
}

View file

@ -1,177 +0,0 @@
package com.pixelized.desktop.lwa.ui.composable.scene
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset
import com.pixelized.desktop.lwa.ui.screen.campaign.LocalCampaignLayoutScope
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_foggy_24dp
import lwacharactersheet.composeapp.generated.resources.ic_foggy_filled_24dp
import lwacharactersheet.composeapp.generated.resources.ic_visibility_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.icon_d100
import lwacharactersheet.composeapp.generated.resources.image_dahome_maps
import lwacharactersheet.composeapp.generated.resources.image_dahome_regions
import org.jetbrains.compose.resources.painterResource
@Composable
fun MapScene(
modifier: Modifier = Modifier,
) {
val campaign = LocalCampaignLayoutScope.current
val scope = rememberCoroutineScope()
val fogOfWarEdit = remember { mutableStateOf(true) }
val map = rememberLayoutFromResource(
name = "Dahomé",
resource = Res.drawable.image_dahome_maps,
)
val mapRegionOverlay = rememberLayoutFromResource(
name = "Région",
resource = Res.drawable.image_dahome_regions,
)
val element1 = rememberElementFromResource(
name = "Group",
resource = Res.drawable.icon_d100,
offset = IntOffset(
x = 2128,
y = 1875,
),
)
val element2 = rememberElementFromResource(
name = "End",
resource = Res.drawable.icon_d100,
offset = IntOffset(
x = map.size.width,
y = map.size.height,
)
)
val element3 = rememberElementFromResource(
name = "Start",
resource = Res.drawable.icon_d100,
offset = IntOffset(
x = 0,
y = 0,
)
)
val scene = remember(map, mapRegionOverlay, element1, element2) {
Scene(
camera = Camera(
initialZoom = 1f,
initialOffset = IntOffset(x = -150, y = -120),
),
fogOfWar = FogOfWar(),
layers = listOf(
map,
mapRegionOverlay,
),
elements = listOf(
element1,
element2,
element3,
),
)
}
Scene(
modifier = modifier.onFogOfWarControl(
scope = scope,
enable = fogOfWarEdit.value,
fogOfWar = scene.fogOfWar,
camera = scene.camera,
),
scene = scene,
) {
Row(
modifier = Modifier
.align(alignment = Alignment.BottomStart)
.padding(
start = campaign.leftPanel.value.width,
bottom = campaign.chatOverlay.value.height,
)
) {
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.layers.getOrNull(1)?.let {
it.alpha(alpha = if (it.alpha == 0f) 1f else 0f)
}
}
}
) {
Icon(
painter = painterResource(Res.drawable.ic_visibility_24dp),
contentDescription = null
)
}
IconButton(
onClick = {
fogOfWarEdit.value = fogOfWarEdit.value.not()
}
) {
Icon(
painter = when (fogOfWarEdit.value) {
true -> painterResource(Res.drawable.ic_foggy_filled_24dp)
else -> painterResource(Res.drawable.ic_foggy_24dp)
},
contentDescription = null
)
}
}
}
}

View file

@ -1,56 +1,31 @@
package com.pixelized.desktop.lwa.ui.composable.scene
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
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.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.translate
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.toSize
import com.pixelized.desktop.lwa.ui.composable.scene.camera.Camera
import com.pixelized.desktop.lwa.ui.composable.scene.camera.onCameraControl
import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneElement
import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneLayer
import com.pixelized.desktop.lwa.ui.composable.scene.utils.global
import com.pixelized.desktop.lwa.ui.composable.scene.utils.local
import com.pixelized.desktop.lwa.ui.theme.lwa
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.imageResource
import java.util.UUID
import kotlin.math.sign
@Stable
data class Scene(
val camera: Camera,
val fogOfWar: FogOfWar,
val layers: List<SceneLayer>,
val elements: List<SceneElement>,
) {
@ -64,35 +39,21 @@ data class Scene(
@Composable
fun Scene(
modifier: Modifier = Modifier,
camera: Camera,
scene: Scene,
content: @Composable BoxScope.() -> Unit,
) {
val scope = rememberCoroutineScope()
val cursors = remember {
listOf(
Cursor()
)
}
Box(
modifier = modifier
modifier = Modifier
.graphicsLayer { clip = true }
.onCameraControl(scope = scope, scene = scene)
.onCursorControl(camera = scene.camera, cursor = cursors.first())
.drawLayers(camera = scene.camera, layers = scene.layers)
.drawElements(camera = scene.camera, elements = scene.elements)
.drawCursors(camera = scene.camera, cursors = cursors)
.drawFogOfWar(scene = scene),
.onCameraControl(scope = scope, sceneSize = scene.size, camera = camera)
.drawLayers(camera = camera, layers = scene.layers)
.drawElements(camera = camera, elements = scene.elements)
.then(other = modifier),
) {
content()
SceneDebugPanel(
modifier = Modifier
.align(alignment = Alignment.TopEnd)
.padding(paddingValues = MaterialTheme.lwa.dimen.paddingValues),
cursor = cursors.first(),
scene = scene,
)
}
}
@ -138,83 +99,6 @@ fun rememberElementFromResource(
}
}
@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,
)
}
}
}
@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.onCursorControl(
camera: Camera,
cursor: Cursor,
): Modifier = this
.onPointerEvent(PointerEventType.Exit) { event: PointerEvent ->
cursor.change(
position = Offset.Unspecified,
)
}
.onPointerEvent(PointerEventType.Move) { event: PointerEvent ->
cursor.change(
position = event.changes.first().position.global(camera = camera),
)
}
private fun Modifier.drawCursors(
camera: Camera,
cursors: List<Cursor>,
): Modifier = this
.drawWithCache {
onDrawBehind {
cursors.forEach { cursor ->
drawRect(
color = Color.Green,
topLeft = cursor.offset.local(camera = camera),
size = Size(10f, 10f),
style = Stroke(width = 2f),
)
}
}
}
@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.drawLayers(
camera: Camera,
@ -254,61 +138,4 @@ fun Modifier.drawElements(
)
}
}
}
fun Modifier.drawFogOfWar(
scene: Scene,
): Modifier = this
.graphicsLayer(
compositingStrategy = CompositingStrategy.Offscreen
)
.drawWithCache {
val stroke = Stroke(
width = 32f,
cap = StrokeCap.Round,
join = StrokeJoin.Round
)
val fog = Color.Black.copy(alpha = 0.5f)
val color = Color.Transparent
onDrawBehind {
drawRect(
color = fog,
)
scale(
scale = 1 / scene.camera.zoom,
pivot = Offset.Zero,
) {
translate(
left = -scene.camera.offset.x.toFloat(),
top = -scene.camera.offset.y.toFloat(),
) {
drawPath(
path = scene.fogOfWar.path,
style = stroke,
color = color,
blendMode = BlendMode.Clear,
)
}
}
}
}
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

@ -0,0 +1,97 @@
package com.pixelized.desktop.lwa.ui.composable.scene.camera
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(
power: Float,
snap: Boolean = false,
) {
val value = _zoom.value * (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,75 @@
package com.pixelized.desktop.lwa.ui.composable.scene.camera
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventType
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.IntSize
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.math.sign
@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
fun Modifier.onCameraControl(
scope: CoroutineScope,
sceneSize: IntSize,
camera: Camera,
): Modifier {
val offsetDelta = CursorDelta()
return this
.onSizeChanged {
camera.changeSizes(
sceneSize = sceneSize,
cameraSize = it,
)
}
.onPointerEvent(PointerEventType.Move) { event: PointerEvent ->
scope.launch {
offsetDelta.handlePositionChange(
event = event,
) { delta ->
when {
event.buttons.isTertiaryPressed || (event.keyboardModifiers.isCtrlPressed && event.buttons.isPrimaryPressed) -> camera.handlePanning(
delta = delta,
snap = true,
)
}
}
}
}
.onPointerEvent(PointerEventType.Scroll) { event: PointerEvent ->
scope.launch {
camera.handleZoom(
power = -event.changes.first().scrollDelta.y.sign * 0.15f,
snap = false,
)
}
}
}
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

@ -0,0 +1,24 @@
package com.pixelized.desktop.lwa.ui.composable.scene.cursor
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntOffset
@Stable
class Cursor(
initial: IntOffset = IntOffset.Zero,
) {
private val _offset = mutableStateOf(initial)
val offset by _offset
fun change(
position: Offset,
) {
_offset.value = IntOffset(
x = position.x.toInt(),
y = position.y.toInt(),
)
}
}

View file

@ -0,0 +1,26 @@
package com.pixelized.desktop.lwa.ui.composable.scene.cursor
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import com.pixelized.desktop.lwa.ui.composable.scene.camera.Camera
import com.pixelized.desktop.lwa.ui.composable.scene.utils.local
fun Modifier.drawCursors(
camera: Camera,
cursors: List<Cursor>,
): Modifier = this
.drawWithCache {
onDrawBehind {
cursors.forEach { cursor ->
drawRect(
color = Color.Green,
topLeft = cursor.offset.local(camera = camera),
size = Size(10f, 10f),
style = Stroke(width = 2f),
)
}
}
}

View file

@ -0,0 +1,26 @@
package com.pixelized.desktop.lwa.ui.composable.scene.cursor
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.onPointerEvent
import com.pixelized.desktop.lwa.ui.composable.scene.camera.Camera
import com.pixelized.desktop.lwa.ui.composable.scene.utils.global
@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.onCursorControl(
camera: Camera,
cursor: Cursor,
): Modifier = this
.onPointerEvent(PointerEventType.Exit) { event: PointerEvent ->
cursor.change(
position = Offset.Unspecified,
)
}
.onPointerEvent(PointerEventType.Move) { event: PointerEvent ->
cursor.change(
position = event.changes.first().position.global(camera = camera),
)
}

View file

@ -0,0 +1,82 @@
package com.pixelized.desktop.lwa.ui.composable.scene.debug
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.scene.camera.Camera
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography
@Composable
fun CameraDebugPanel(
modifier: Modifier = Modifier,
style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug,
padding: Dp = MaterialTheme.lwa.dimen.debug.offset,
spacing: Dp = 2.dp,
camera: Camera,
isOpen: Boolean = true,
) {
val isOpen = remember { mutableStateOf(isOpen) }
Column(
modifier = Modifier
.clickable { isOpen.value = isOpen.value.not() }
.widthIn(min = MaterialTheme.lwa.dimen.debug.panelWidth)
.animateContentSize()
.then(other = modifier),
verticalArrangement = Arrangement.spacedBy(space = spacing),
) {
Text(
style = MaterialTheme.lwa.typography.debug.title,
color = MaterialTheme.lwa.colorScheme.base.primary,
text = "Camera",
)
if (isOpen.value) {
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyIdSpan) { append("offset: ") }
append(camera.offset.toString())
},
)
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyIdSpan) { append("size: ") }
append(camera.cameraSize.toString())
},
)
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyIdSpan) { append("zoom: ") }
append(camera.zoom.toString())
},
)
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyIdSpan) { append("projection: ") }
append(camera.cameraSizeZoomed.toString())
},
)
}
}
}

View file

@ -1,5 +1,6 @@
package com.pixelized.desktop.lwa.ui.composable.scene
package com.pixelized.desktop.lwa.ui.composable.scene.debug
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -8,44 +9,25 @@ import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.scene.cursor.Cursor
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography
@Stable
class Cursor(
initial: IntOffset = IntOffset.Zero,
) {
private val _offset = mutableStateOf(initial)
val offset by _offset
fun change(
position: Offset,
) {
_offset.value = IntOffset(
x = position.x.toInt(),
y = position.y.toInt(),
)
}
}
@Composable
fun SceneCursorDebug(
fun CursorDebugPanel(
modifier: Modifier = Modifier,
cursor: Cursor,
isOpen: Boolean = true,
style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug,
padding: Dp = MaterialTheme.lwa.dimen.debug.offset,
spacing: Dp = 2.dp,
cursors: List<Cursor>,
isOpen: Boolean = true,
) {
val isOpen = remember { mutableStateOf(isOpen) }
@ -53,8 +35,9 @@ fun SceneCursorDebug(
modifier = Modifier
.clickable { isOpen.value = isOpen.value.not() }
.widthIn(min = MaterialTheme.lwa.dimen.debug.panelWidth)
.animateContentSize()
.then(other = modifier),
verticalArrangement = Arrangement.spacedBy(space = 2.dp),
verticalArrangement = Arrangement.spacedBy(space = spacing),
) {
Text(
style = MaterialTheme.lwa.typography.debug.title,
@ -62,14 +45,16 @@ fun SceneCursorDebug(
text = "Cursor",
)
if (isOpen.value) {
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("offset: ") }
append(cursor.offset.toString())
},
)
cursors.forEach { cursor ->
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyIdSpan) { append("coordinate: ") }
append(cursor.offset.toString())
},
)
}
}
}
}

View file

@ -1,15 +1,11 @@
package com.pixelized.desktop.lwa.ui.composable.scene
package com.pixelized.desktop.lwa.ui.composable.scene.debug
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@ -20,6 +16,7 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.scene.Scene
import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneElementDebug
import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneLayerDebug
import com.pixelized.desktop.lwa.ui.theme.lwa
@ -28,74 +25,11 @@ import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography
@Composable
fun SceneDebugPanel(
modifier: Modifier = Modifier,
paddingValues: PaddingValues = MaterialTheme.lwa.dimen.paddingValues,
cursor: Cursor,
scene: Scene,
) {
Card(
modifier = Modifier
.verticalScroll(state = rememberScrollState())
.then(other = modifier),
backgroundColor = MaterialTheme.lwa.colorScheme.elevated.base4dp,
) {
Column(
modifier = Modifier.padding(paddingValues = paddingValues),
verticalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
SceneDebug(
modifier = Modifier.animateContentSize(),
scene = scene,
)
SceneCursorDebug(
modifier = Modifier.animateContentSize(),
cursor = cursor,
)
SceneCameraDebug(
modifier = Modifier.animateContentSize(),
camera = scene.camera,
)
Column {
Text(
style = MaterialTheme.lwa.typography.debug.title,
text = "Layers:(${scene.layers.size})"
)
scene.layers.forEach { layer ->
SceneLayerDebug(
modifier = Modifier
.animateContentSize()
.padding(start = MaterialTheme.lwa.dimen.debug.offset),
isOpen = false,
layer = layer,
)
}
}
Column {
Text(
style = MaterialTheme.lwa.typography.debug.title,
text = "Elements:(${scene.elements.size})"
)
scene.elements.forEach { element ->
SceneElementDebug(
modifier = Modifier
.animateContentSize()
.padding(start = MaterialTheme.lwa.dimen.debug.offset),
isOpen = false,
element = element,
)
}
}
}
}
}
@Composable
private fun SceneDebug(
modifier: Modifier = Modifier,
scene: Scene,
isOpen: Boolean = true,
style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug,
padding: Dp = MaterialTheme.lwa.dimen.debug.offset,
spacing: Dp = 2.dp,
scene: Scene,
isOpen: Boolean = true,
) {
val isOpen = remember { mutableStateOf(isOpen) }
@ -103,8 +37,9 @@ private fun SceneDebug(
modifier = Modifier
.clickable { isOpen.value = isOpen.value.not() }
.widthIn(min = MaterialTheme.lwa.dimen.debug.panelWidth)
.animateContentSize()
.then(other = modifier),
verticalArrangement = Arrangement.spacedBy(space = 2.dp),
verticalArrangement = Arrangement.spacedBy(space = spacing),
) {
Text(
style = MaterialTheme.lwa.typography.debug.title,
@ -116,10 +51,40 @@ private fun SceneDebug(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("Size: ") }
withStyle(style.propertyIdSpan) { append("Size: ") }
append(scene.size.toString())
},
)
Column(
modifier = Modifier.padding(start = padding),
) {
Text(
style = MaterialTheme.lwa.typography.debug.propertyId,
text = "Layers:(${scene.layers.size})"
)
scene.layers.forEach { layer ->
SceneLayerDebug(
modifier = Modifier.padding(start = padding),
isOpen = false,
layer = layer,
)
}
}
Column(
modifier = Modifier.padding(start = padding),
) {
Text(
style = MaterialTheme.lwa.typography.debug.propertyId,
text = "Elements:(${scene.elements.size})"
)
scene.elements.forEach { element ->
SceneElementDebug(
modifier = Modifier.padding(start = padding),
isOpen = false,
element = element,
)
}
}
}
}
}

View file

@ -71,7 +71,7 @@ fun SceneElementDebug(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("id: ") }
withStyle(style.propertyIdSpan) { append("id: ") }
append(element.id)
},
)
@ -79,7 +79,7 @@ fun SceneElementDebug(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("offset: ") }
withStyle(style.propertyIdSpan) { append("offset: ") }
append(element.offset.toString())
},
)
@ -87,7 +87,7 @@ fun SceneElementDebug(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("size: ") }
withStyle(style.propertyIdSpan) { append("size: ") }
append(element.size.toString())
},
)
@ -95,7 +95,7 @@ fun SceneElementDebug(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("alpha: ") }
withStyle(style.propertyIdSpan) { append("alpha: ") }
append(element.alpha.toString())
},
)

View file

@ -57,7 +57,7 @@ fun SceneLayerDebug(
verticalArrangement = Arrangement.spacedBy(space = 2.dp),
) {
Text(
style = MaterialTheme.lwa.typography.debug.title,
style = MaterialTheme.lwa.typography.debug.propertyId,
color = MaterialTheme.lwa.colorScheme.base.primary,
text = layer.name,
)
@ -66,7 +66,7 @@ fun SceneLayerDebug(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("id: ") }
withStyle(style.propertyIdSpan) { append("id: ") }
append(layer.id)
},
)
@ -74,7 +74,7 @@ fun SceneLayerDebug(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("offset: ") }
withStyle(style.propertyIdSpan) { append("offset: ") }
append(layer.offset.toString())
},
)
@ -82,7 +82,7 @@ fun SceneLayerDebug(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("size: ") }
withStyle(style.propertyIdSpan) { append("size: ") }
append(layer.size.toString())
},
)
@ -90,7 +90,7 @@ fun SceneLayerDebug(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("alpha: ") }
withStyle(style.propertyIdSpan) { append("alpha: ") }
append(layer.alpha.toString())
},
)

View file

@ -0,0 +1,44 @@
package com.pixelized.desktop.lwa.ui.composable.scene.fogOfWar
import androidx.compose.runtime.Stable
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.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.translate
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.isPrimaryPressed
import androidx.compose.ui.input.pointer.onPointerEvent
import com.pixelized.desktop.lwa.ui.composable.scene.camera.Camera
import com.pixelized.desktop.lwa.ui.composable.scene.utils.global
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Stable
class FogOfWar(
val color: Color = Color.Black.copy(alpha = 0.5f),
) {
val path = Path()
fun moveTo(position: Offset) {
path.moveTo(x = position.x, y = position.y)
}
fun lineTo(position: Offset) {
path.lineTo(x = position.x, y = position.y)
}
companion object {
val NONE = FogOfWar(color = Color.Transparent)
}
}

View file

@ -0,0 +1,53 @@
package com.pixelized.desktop.lwa.ui.composable.scene.fogOfWar
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.graphicsLayer
import com.pixelized.desktop.lwa.ui.composable.scene.camera.Camera
fun Modifier.drawFogOfWar(
camera: Camera,
fogOfWar: FogOfWar,
): Modifier = this
.graphicsLayer(
compositingStrategy = CompositingStrategy.Offscreen
)
.drawWithCache {
val stroke = Stroke(
width = 32f,
cap = StrokeCap.Round,
join = StrokeJoin.Round
)
val fog = Color.Black.copy(alpha = 0.5f)
val color = Color.Transparent
onDrawBehind {
drawRect(
color = fog,
)
scale(
scale = 1 / camera.zoom,
pivot = Offset.Zero,
) {
translate(
left = -camera.offset.x.toFloat(),
top = -camera.offset.y.toFloat(),
) {
drawPath(
path = fogOfWar.path,
style = stroke,
color = color,
blendMode = BlendMode.Clear,
)
}
}
}
}

View file

@ -1,38 +1,16 @@
package com.pixelized.desktop.lwa.ui.composable.scene
package com.pixelized.desktop.lwa.ui.composable.scene.fogOfWar
import androidx.compose.runtime.Stable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.isPrimaryPressed
import androidx.compose.ui.input.pointer.onPointerEvent
import com.pixelized.desktop.lwa.ui.composable.scene.camera.Camera
import com.pixelized.desktop.lwa.ui.composable.scene.utils.global
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Stable
class FogOfWar(
val color: Color = Color.Black.copy(alpha = 0.5f),
) {
val path = Path()
fun moveTo(position: Offset) {
path.moveTo(x = position.x, y = position.y)
}
fun lineTo(position: Offset) {
path.lineTo(x = position.x, y = position.y)
}
companion object {
val NONE = FogOfWar(color = Color.Transparent)
}
}
@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.onFogOfWarControl(
scope: CoroutineScope,
@ -46,7 +24,7 @@ fun Modifier.onFogOfWarControl(
var previousEvent: PointerEvent? = null
this
.onPointerEvent(PointerEventType.Release) { event: PointerEvent ->
.onPointerEvent(PointerEventType.Release) { _: PointerEvent ->
scope.launch {
println("PointerEventType.Release")
lastEventTime = System.currentTimeMillis()

View file

@ -2,7 +2,7 @@ package com.pixelized.desktop.lwa.ui.composable.scene.utils
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntOffset
import com.pixelized.desktop.lwa.ui.composable.scene.Camera
import com.pixelized.desktop.lwa.ui.composable.scene.camera.Camera
/**
* Convert local positon to global one.

View file

@ -34,7 +34,7 @@ class WindowController(
private val _windows = mutableStateOf<Map<String, Window>>(emptyMap())
val windows: State<Map<String, Window>> get() = _windows
fun showWindow(window: Window) {
fun openWindow(window: Window) {
_windows.value = _windows.value.toMutableMap().apply { this[window.id] = window }
}

View file

@ -14,10 +14,10 @@ class GameMasterWindow(
size = size,
)
fun WindowController.navigateToGameMasterWindow(
fun WindowController.openGameMasterWindow(
title: String = "Game master",
) {
showWindow(
openWindow(
window = GameMasterWindow(
title = title,
size = DpSize(

View file

@ -44,7 +44,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.MapScene
import com.pixelized.desktop.lwa.ui.screen.campaign.map.MapScene
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,8 +55,8 @@ import com.pixelized.desktop.lwa.ui.screen.campaign.player.detail.rememberTransi
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.chatbox.CampaignChat
import com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.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
@ -101,9 +101,7 @@ fun CampaignScreen(
},
main = {
MapScene(
modifier = Modifier.matchParentSize(),
)
},
chat = {
CampaignChat(

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.text
package com.pixelized.desktop.lwa.ui.screen.campaign.chatbox
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
@ -42,15 +42,15 @@ import androidx.compose.ui.window.WindowState
import com.pixelized.desktop.lwa.ui.navigation.window.LocalWindowState
import com.pixelized.desktop.lwa.ui.screen.campaign.CampaignLayoutScope
import com.pixelized.desktop.lwa.ui.screen.campaign.LocalCampaignLayoutScope
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.CharacteristicTextMessage
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.CharacteristicTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.DiminishedTextMessage
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.DiminishedTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.PurseTextMessage
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.PurseTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.RollTextMessage
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.RollTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.TextMessage
import com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.messages.CharacteristicTextMessage
import com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.messages.CharacteristicTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.messages.DiminishedTextMessage
import com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.messages.DiminishedTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.messages.PurseTextMessage
import com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.messages.PurseTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.messages.RollTextMessage
import com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.messages.RollTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.messages.TextMessage
import com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.usecase.SettingsUseCase
import lwacharactersheet.composeapp.generated.resources.Res

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.text
package com.pixelized.desktop.lwa.ui.screen.campaign.chatbox
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope

View file

@ -1,12 +1,12 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.text
package com.pixelized.desktop.lwa.ui.screen.campaign.chatbox
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.settings.model.Settings
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.CharacteristicTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.DiminishedTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.PurseTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.RollTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.text.messages.TextMessage
import com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.messages.CharacteristicTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.messages.DiminishedTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.messages.PurseTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.messages.RollTextMessageUio
import com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.messages.TextMessage
import com.pixelized.shared.lwa.model.campaign.Campaign
import com.pixelized.shared.lwa.protocol.websocket.ApiSynchronisation
import com.pixelized.shared.lwa.protocol.websocket.CampaignEvent

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.text.messages
package com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.messages
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.text.messages
package com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.messages
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.text.messages
package com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.messages
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.TooltipPlacement
@ -8,7 +8,6 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.text.messages
package com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.messages
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row

View file

@ -1,4 +1,4 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.text.messages
package com.pixelized.desktop.lwa.ui.screen.campaign.chatbox.messages
sealed interface TextMessage {
val id : String

View file

@ -0,0 +1,205 @@
package com.pixelized.desktop.lwa.ui.screen.campaign.map
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.desktop.lwa.ui.composable.scene.Scene
import com.pixelized.desktop.lwa.ui.composable.scene.camera.Camera
import com.pixelized.desktop.lwa.ui.composable.scene.cursor.Cursor
import com.pixelized.desktop.lwa.ui.composable.scene.cursor.onCursorControl
import com.pixelized.desktop.lwa.ui.composable.scene.debug.CameraDebugPanel
import com.pixelized.desktop.lwa.ui.composable.scene.debug.CursorDebugPanel
import com.pixelized.desktop.lwa.ui.composable.scene.debug.SceneDebugPanel
import com.pixelized.desktop.lwa.ui.composable.scene.rememberLayoutFromResource
import com.pixelized.desktop.lwa.ui.theme.lwa
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_frame_bug_24dp
import lwacharactersheet.composeapp.generated.resources.ic_visibility_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.image_dahome_maps
import lwacharactersheet.composeapp.generated.resources.image_dahome_regions
import org.jetbrains.compose.resources.painterResource
@Composable
fun MapScene(
modifier: Modifier = Modifier,
enableDebug: Boolean,
) {
val scope = rememberCoroutineScope()
val map = rememberLayoutFromResource(
name = "Dahomé",
resource = Res.drawable.image_dahome_maps,
)
val mapRegionOverlay = rememberLayoutFromResource(
name = "Région",
resource = Res.drawable.image_dahome_regions,
)
val camera = remember {
Camera(
initialZoom = 1f,
initialOffset = IntOffset(x = -150, y = -120),
)
}
val cursor = remember {
Cursor()
}
val scene = remember(map, mapRegionOverlay) {
Scene(
layers = listOf(map, mapRegionOverlay),
elements = emptyList(),
)
}
val openDebugMenu = remember {
mutableStateOf(false)
}
Scene(
modifier = Modifier
.onCursorControl(camera = camera, cursor = cursor)
.then(other = modifier),
scene = scene,
camera = camera,
) {
MapActions(
modifier = Modifier
.align(alignment = Alignment.BottomEnd)
.padding(paddingValues = MaterialTheme.lwa.dimen.paddingValues),
enableDebug = enableDebug,
onZoomIn = {
scope.launch {
camera.handleZoom(power = 0.3f)
}
},
onZoomOut = {
scope.launch {
camera.handleZoom(power = -0.3f)
}
},
onResetCamera = {
scope.launch {
camera.resetPosition()
}
scope.launch {
camera.resetZoom()
}
},
onToggleLayer = {
scope.launch {
scene.layers.getOrNull(1)?.let {
it.alpha(alpha = if (it.alpha == 0f) 1f else 0f)
}
}
},
onToggleDebug = {
openDebugMenu.value = openDebugMenu.value.not()
},
)
AnimatedVisibility(
modifier = Modifier.align(alignment = Alignment.TopEnd),
visible = openDebugMenu.value,
) {
Card(
modifier = Modifier
.padding(paddingValues = MaterialTheme.lwa.dimen.paddingValues)
.verticalScroll(state = rememberScrollState()),
backgroundColor = MaterialTheme.lwa.colorScheme.elevated.base4dp,
) {
Column(
modifier = Modifier.padding(paddingValues = MaterialTheme.lwa.dimen.paddingValues),
verticalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
SceneDebugPanel(
scene = scene,
)
CameraDebugPanel(
camera = camera,
)
CursorDebugPanel(
cursors = remember { listOf(cursor) },
)
}
}
}
}
}
@Composable
private fun MapActions(
modifier: Modifier = Modifier,
enableDebug: Boolean,
onZoomIn: () -> Unit,
onZoomOut: () -> Unit,
onResetCamera: () -> Unit,
onToggleLayer: () -> Unit,
onToggleDebug: () -> Unit,
) {
Row(
modifier = modifier,
) {
IconButton(
onClick = onZoomIn,
) {
Icon(
painter = painterResource(Res.drawable.ic_zoom_in_map_24dp),
contentDescription = null
)
}
IconButton(
onClick = onZoomOut,
) {
Icon(
painter = painterResource(Res.drawable.ic_zoom_out_map_24dp),
contentDescription = null
)
}
IconButton(
onClick = onResetCamera,
) {
Icon(
painter = painterResource(Res.drawable.ic_center_focus_weak_24dp),
contentDescription = null
)
}
IconButton(
onClick = onToggleLayer,
) {
Icon(
painter = painterResource(Res.drawable.ic_visibility_24dp),
contentDescription = null
)
}
AnimatedVisibility(
visible = enableDebug,
) {
IconButton(
onClick = onToggleDebug,
) {
Icon(
painter = painterResource(Res.drawable.ic_frame_bug_24dp),
contentDescription = null
)
}
}
}
}

View file

@ -13,25 +13,25 @@ import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.pixelized.desktop.lwa.LocalWindowController
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToSettings
import com.pixelized.desktop.lwa.ui.navigation.window.destination.navigateToGameMasterWindow
import com.pixelized.desktop.lwa.ui.navigation.window.destination.openGameMasterWindow
import com.pixelized.desktop.lwa.ui.screen.campaign.map.MapScene
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.links.ResourcesMenu
import com.pixelized.desktop.lwa.ui.screen.campaign.toolbar.network.NetworkMenu
import com.pixelized.desktop.lwa.ui.theme.lwa
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.ic_link_24dp
import lwacharactersheet.composeapp.generated.resources.ic_map_24dp
import lwacharactersheet.composeapp.generated.resources.ic_settings_24dp
import lwacharactersheet.composeapp.generated.resources.ic_wifi_24dp
import lwacharactersheet.composeapp.generated.resources.ic_wifi_off_24dp
@ -41,9 +41,10 @@ import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
@Stable
data class MenuState(
val isResourcesMenuOpen: MutableState<Boolean> = mutableStateOf(false),
val isNetworkMenuOpen: MutableState<Boolean> = mutableStateOf(false),
data class CampaignMenuStateUio(
val isResourcesMenuOpen: Boolean,
val isNetworkMenuOpen: Boolean,
val isMapMenuOpen: Boolean,
)
@Composable
@ -53,51 +54,47 @@ fun CampaignToolbar(
val windows = LocalWindowController.current
val screen = LocalScreenController.current
val menuState = remember { MenuState() }
val title = viewModel.title.collectAsState()
val status = viewModel.status.collectAsState()
val isAdmin = viewModel.isAdmin.collectAsState()
val title = viewModel.title.collectAsStateWithLifecycle()
val status = viewModel.status.collectAsStateWithLifecycle()
val isAdmin = viewModel.isAdmin.collectAsStateWithLifecycle()
val menusState = viewModel.menusState.collectAsStateWithLifecycle()
CampaignToolbarContent(
title = title,
status = status,
isAdmin = isAdmin,
state = menuState,
menusState = menusState,
onAdmin = {
windows.navigateToGameMasterWindow()
},
onNetwork = {
menuState.isNetworkMenuOpen.value = true
},
onResources = {
menuState.isResourcesMenuOpen.value = true
windows.openGameMasterWindow()
},
onSettings = {
screen.navigateToSettings()
},
onDismissNetworkMenu = {
menuState.isNetworkMenuOpen.value = false
},
onDismissResourcesMenu = {
menuState.isResourcesMenuOpen.value = false
},
onNetwork = viewModel::onNetwork,
onResources = viewModel::onResources,
onMap = viewModel::onMap,
onDismissNetworkMenu = viewModel::onDismissNetworkMenu,
onDismissResourcesMenu = viewModel::onDismissResourcesMenu,
onDismissMapMenu = viewModel::onDismissMapMenu,
)
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun CampaignToolbarContent(
modifier: Modifier = Modifier,
title: State<String>,
status: State<NetworkRepository.Status>,
isAdmin: State<Boolean>,
state: MenuState,
menusState: State<CampaignMenuStateUio>,
onAdmin: () -> Unit,
onNetwork: () -> Unit,
onMap: () -> Unit,
onResources: () -> Unit,
onSettings: () -> Unit,
onDismissNetworkMenu: () -> Unit,
onDismissResourcesMenu: () -> Unit,
onDismissMapMenu: () -> Unit,
) {
TopAppBar(
modifier = modifier,
@ -121,6 +118,25 @@ private fun CampaignToolbarContent(
)
}
}
IconButton(
onClick = onMap,
) {
Icon(
painter = painterResource(Res.drawable.ic_map_24dp),
tint = MaterialTheme.colors.primary,
contentDescription = null,
)
DropdownMenu(
expanded = menusState.value.isMapMenuOpen,
onDismissRequest = onDismissMapMenu,
content = {
MapScene(
modifier = Modifier.size(640.dp),
enableDebug = isAdmin.value,
)
},
)
}
IconButton(
onClick = onResources,
) {
@ -130,7 +146,7 @@ private fun CampaignToolbarContent(
contentDescription = null,
)
DropdownMenu(
expanded = state.isResourcesMenuOpen.value,
expanded = menusState.value.isResourcesMenuOpen,
onDismissRequest = onDismissResourcesMenu,
content = {
ResourcesMenu(
@ -156,7 +172,7 @@ private fun CampaignToolbarContent(
contentDescription = null,
)
DropdownMenu(
expanded = state.isNetworkMenuOpen.value,
expanded = menusState.value.isNetworkMenuOpen,
onDismissRequest = onDismissNetworkMenu,
content = {
NetworkMenu(

View file

@ -3,20 +3,30 @@ package com.pixelized.desktop.lwa.ui.screen.campaign.toolbar
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.network.NetworkRepository
import com.pixelized.desktop.lwa.repository.settings.SettingsRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
class CampaignToolbarViewModel(
private val characterRepository: CharacterSheetRepository,
private val campaignRepository: CampaignRepository,
campaignRepository: CampaignRepository,
networkRepository: NetworkRepository,
settingsRepository: SettingsRepository,
) : ViewModel() {
private val _menusState = MutableStateFlow(
CampaignMenuStateUio(
isResourcesMenuOpen = false,
isNetworkMenuOpen = false,
isMapMenuOpen = false
)
)
val menusState: StateFlow<CampaignMenuStateUio> = _menusState
val status = networkRepository.status
val title = campaignRepository.campaignFlow()
@ -34,4 +44,36 @@ class CampaignToolbarViewModel(
started = SharingStarted.Lazily,
initialValue = false,
)
val isGameMaster = settingsRepository.settingsFlow()
.map { it.isGameMaster ?: false }
.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = false,
)
fun onNetwork() {
_menusState.update { it.copy(isNetworkMenuOpen = true) }
}
fun onResources() {
_menusState.update { it.copy(isResourcesMenuOpen = true) }
}
fun onMap() {
_menusState.update { it.copy(isMapMenuOpen = true) }
}
fun onDismissNetworkMenu() {
_menusState.update { it.copy(isNetworkMenuOpen = false) }
}
fun onDismissResourcesMenu() {
_menusState.update { it.copy(isResourcesMenuOpen = false) }
}
fun onDismissMapMenu() {
_menusState.update { it.copy(isMapMenuOpen = false) }
}
}

View file

@ -79,7 +79,8 @@ data class LwaTypography(
@Stable
data class Debug(
val title: TextStyle,
val propertyId: SpanStyle,
val propertyId: TextStyle,
val propertyIdSpan: SpanStyle,
val propertyValue: TextStyle,
)
}
@ -216,6 +217,11 @@ fun lwaTypography(
fontWeight = FontWeight.Normal,
fontSize = 10.sp,
lineHeight = 14.sp,
),
propertyIdSpan = robotoMono.caption.copy(
fontWeight = FontWeight.Normal,
fontSize = 10.sp,
lineHeight = 14.sp,
).toSpanStyle(),
propertyValue = robotoMono.caption.copy(
fontWeight = FontWeight.Normal,