Add a debug panel to the scene + MapScene + Cursor

This commit is contained in:
Thomas Andres Gomez 2025-10-24 19:04:08 +02:00
parent 4e013aa358
commit ae2c3da582
13 changed files with 858 additions and 153 deletions

View file

@ -2,15 +2,32 @@ 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(
@ -99,3 +116,62 @@ class Camera(
}
}
}
@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

@ -0,0 +1,75 @@
package com.pixelized.desktop.lwa.ui.composable.scene
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.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.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(
modifier: Modifier = Modifier,
cursor: Cursor,
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 = "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())
},
)
}
}
}

View file

@ -0,0 +1,154 @@
package com.pixelized.desktop.lwa.ui.composable.scene
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.runtime.Composable
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 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_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 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.NONE,
layers = listOf(
map,
mapRegionOverlay,
),
elements = listOf(
element1,
element2,
element3,
),
)
}
Scene(
modifier = modifier,
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.layers.getOrNull(1)?.let {
it.alpha(alpha = if (it.alpha == 0f) 1f else 0f)
}
}
}
) {
Icon(
painter = painterResource(Res.drawable.ic_visibility_24dp),
contentDescription = null
)
}
}
}
}

View file

@ -5,5 +5,9 @@ import androidx.compose.ui.graphics.Color
@Stable
data class FogOfWar(
val color: Color = Color.Black.copy(alpha = 0.5f),
)
val color: Color = Color.Black.copy(alpha = 0.0f),
) {
companion object {
val NONE = FogOfWar(color = Color.Transparent)
}
}

View file

@ -2,12 +2,9 @@ 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.BoxScope
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.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
@ -17,6 +14,9 @@ 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.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventType
@ -28,163 +28,107 @@ 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 androidx.compose.ui.unit.toSize
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 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 java.util.UUID
import kotlin.math.sign
@Stable
data class Scene(
val camera: Camera,
val layouts: List<Layout>,
val fogOfWar: FogOfWar,
val layers: List<SceneLayer>,
val elements: List<SceneElement>,
) {
val size: IntSize = IntSize(
width = layouts.maxOf { it.size.width },
height = layouts.maxOf { it.size.height },
width = layers.maxOf { it.size.width },
height = layers.maxOf { it.size.height },
)
}
@Composable
fun Scene(
modifier: Modifier,
modifier: Modifier = Modifier,
scene: Scene,
content: @Composable BoxScope.() -> Unit,
) {
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),
),
val cursors = remember {
listOf(
Cursor()
)
}
Box(
modifier = modifier
.graphicsLayer { clip = true }
.onCameraControl(scope = scope, scene = scene)
.drawScene(scene = scene)
.fogOfWar(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),
) {
Column(
content()
SceneDebugPanel(
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,
.align(alignment = Alignment.TopEnd)
.padding(paddingValues = MaterialTheme.lwa.dimen.paddingValues),
cursor = cursors.first(),
scene = scene,
)
}
}
) {
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(
name: String,
resource: DrawableResource,
offset: IntOffset = IntOffset.Zero,
): Layout {
): SceneLayer {
val texture = imageResource(
resource = resource,
)
return remember(resource) {
Layout(
SceneLayer(
id = UUID.randomUUID().toString(),
name = name,
texture = texture,
offset = offset,
alpha = 1f,
)
}
}
@Composable
@Stable
fun rememberScene(
camera: Camera,
fogOfWar: FogOfWar,
vararg layouts: Layout,
): Scene {
return remember {
Scene(
camera = camera,
layouts = layouts.toList(),
fogOfWar = fogOfWar,
fun rememberElementFromResource(
name: String,
resource: DrawableResource,
offset: IntOffset = IntOffset.Zero,
): SceneElement {
val texture = imageResource(
resource = resource,
)
return remember(resource) {
SceneElement(
id = UUID.randomUUID().toString(),
name = name,
texture = texture,
offset = offset,
alpha = 1f,
)
}
}
@ -233,23 +177,81 @@ fun Modifier.onCameraControl(
}
}
fun Modifier.drawScene(
scene: Scene,
): Modifier = this.drawWithCache {
@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 {
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,
cursors.forEach { cursor ->
drawRect(
color = Color.Green,
topLeft = cursor.offset.local(camera = camera),
size = Size(10f, 10f),
style = Stroke(width = 2f),
)
}
}
}
fun Modifier.fogOfWar(
@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.drawLayers(
camera: Camera,
layers: List<SceneLayer>,
): Modifier = this.drawWithCache {
onDrawBehind {
layers.forEach { layers ->
drawImage(
image = layers.texture,
srcOffset = camera.offset - layers.offset,
srcSize = camera.cameraSizeZoomed,
dstSize = camera.cameraSize,
alpha = layers.alpha,
)
}
}
}
fun Modifier.drawElements(
camera: Camera,
elements: List<SceneElement>,
): Modifier = this.drawWithCache {
onDrawBehind {
elements.forEach { element ->
drawImage(
image = element.texture,
srcOffset = camera.offset - element.position,
srcSize = camera.cameraSizeZoomed,
dstSize = camera.cameraSize,
alpha = element.alpha,
)
drawRect(
color = Color.Red,
topLeft = element.position.local(camera = camera),
size = (element.size).toSize() / camera.zoom,
style = Stroke(width = 2f),
)
}
}
}
fun Modifier.drawFogOfWar(
scene: Scene,
): Modifier = this.drawWithCache {
onDrawBehind {

View file

@ -0,0 +1,114 @@
package com.pixelized.desktop.lwa.ui.composable.scene
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.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
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.drawables.SceneElementDebug
import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneLayerDebug
import com.pixelized.desktop.lwa.ui.theme.lwa
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,
backgroundColor = MaterialTheme.lwa.colorScheme.elevated.base4dp,
) {
Column(
modifier = Modifier.padding(paddingValues = paddingValues),
verticalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
SceneDebug(
scene = scene,
)
SceneCursorDebug(
cursor = cursor,
)
SceneCameraDebug(
camera = scene.camera,
)
Column {
Text(
style = MaterialTheme.lwa.typography.debug.title,
text = "Layers:(${scene.layers.size})"
)
scene.layers.forEach { layer ->
SceneLayerDebug(
modifier = Modifier.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.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,
) {
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 = "Scene",
)
if (isOpen.value) {
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("Size: ") }
append(scene.size.toString())
},
)
}
}
}

View file

@ -1,22 +1,22 @@
package com.pixelized.desktop.lwa.ui.composable.scene
package com.pixelized.desktop.lwa.ui.composable.scene.drawables
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(
open class SceneDrawable(
val id: String,
val name: String,
val texture: ImageBitmap,
val offset: IntOffset = IntOffset.Zero,
val size: IntSize = IntSize(texture.width, texture.height),
private val initialAlpha: Float = 1f,
val offset: IntOffset,
val size: IntSize,
private val initialAlpha: Float,
) {
private val _alpha = Animatable(
initialValue = initialAlpha,
typeConverter = Float.VectorConverter,
typeConverter = Float.Companion.VectorConverter,
)
val alpha get() = _alpha.value

View file

@ -0,0 +1,104 @@
package com.pixelized.desktop.lwa.ui.composable.scene.drawables
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.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
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 com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography
@Stable
class SceneElement(
id: String,
name: String,
texture: ImageBitmap,
offset: IntOffset = IntOffset.Companion.Zero,
size: IntSize = IntSize(texture.width, texture.height),
alpha: Float = 1f,
) : SceneDrawable(
id = id,
name = name,
texture = texture,
offset = offset,
size = size,
initialAlpha = alpha,
) {
val position = IntOffset(
x = offset.x - size.width / 2,
y = offset.y - size.height / 2,
)
}
@Composable
fun SceneElementDebug(
modifier: Modifier = Modifier,
element: SceneElement,
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 = element.name,
)
if (isOpen.value) {
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("id: ") }
append(element.id)
},
)
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("offset: ") }
append(element.offset.toString())
},
)
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("size: ") }
append(element.size.toString())
},
)
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("alpha: ") }
append(element.alpha.toString())
},
)
}
}
}

View file

@ -0,0 +1,99 @@
package com.pixelized.desktop.lwa.ui.composable.scene.drawables
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.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
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 com.pixelized.desktop.lwa.ui.theme.lwa
import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography
@Stable
class SceneLayer(
id : String,
name: String,
texture: ImageBitmap,
offset: IntOffset = IntOffset.Companion.Zero,
size: IntSize = IntSize(texture.width, texture.height),
alpha: Float = 1f,
) : SceneDrawable(
id = id,
name = name,
texture = texture,
offset = offset,
size = size,
initialAlpha = alpha,
)
@Composable
fun SceneLayerDebug(
modifier: Modifier = Modifier,
layer: SceneLayer,
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 = layer.name,
)
if (isOpen.value) {
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("id: ") }
append(layer.id)
},
)
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("offset: ") }
append(layer.offset.toString())
},
)
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("size: ") }
append(layer.size.toString())
},
)
Text(
modifier = Modifier.padding(start = padding),
style = MaterialTheme.lwa.typography.debug.propertyValue,
text = buildAnnotatedString {
withStyle(style.propertyId) { append("alpha: ") }
append(layer.alpha.toString())
},
)
}
}
}

View file

@ -0,0 +1,39 @@
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
/**
* Convert local positon to global one.
* Global position are agnostic from camera, and therefor should be use to position stuff on the map.
* A common use case is to share players cursor.
*/
fun Offset.global(
camera: Camera,
): Offset = Offset(
x = this.x * camera.zoom + camera.offset.x,
y = this.y * camera.zoom + camera.offset.y,
)
/**
* Convert global positon to local one.
* Local position take into account the camera and are use to display stuff on the Scene composable.
*/
fun Offset.local(
camera: Camera,
): Offset = Offset(
x = (this.x - camera.offset.x) / camera.zoom,
y = (this.y - camera.offset.y) / camera.zoom,
)
/**
* Convert global positon to local one.
* Local position take into account the camera and are use to display stuff on the Scene composable.
*/
fun IntOffset.local(
camera: Camera,
): Offset = Offset(
x = (this.x.toFloat() - camera.offset.x) / camera.zoom,
y = (this.y.toFloat() - camera.offset.y) / camera.zoom,
)

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.Scene
import com.pixelized.desktop.lwa.ui.composable.scene.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
@ -101,7 +101,7 @@ fun CampaignScreen(
},
main = {
Scene(
MapScene(
modifier = Modifier.matchParentSize(),
)
},

View file

@ -15,6 +15,7 @@ data class LwaDimen(
val layout: Layout,
val portrait: Portrait,
val sheet: Sheet,
val debug: Debug,
) {
@Stable
data class Layout(
@ -33,6 +34,12 @@ data class LwaDimen(
val subCategory: Dp,
val characteristic: DpSize,
)
@Stable
data class Debug(
val panelWidth: Dp,
val offset: Dp,
)
}
@Composable
@ -52,6 +59,10 @@ fun lwaDimen(
detailWidth = 128.dp * 4,
chatMaxWidth = 600.dp,
),
debug: LwaDimen.Debug = LwaDimen.Debug(
panelWidth = 200.dp,
offset = 8.dp,
)
): LwaDimen {
return remember {
LwaDimen(
@ -60,6 +71,7 @@ fun lwaDimen(
portrait = portrait,
sheet = sheet,
layout = layout,
debug = debug,
)
}
}

View file

@ -10,6 +10,7 @@ import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
@ -30,6 +31,7 @@ data class LwaTypography(
val characterSheet: CharacterSheet,
val inventory: Inventory,
val freeDiceThrow: Dice,
val debug: Debug,
) {
@Stable
data class Chat(
@ -73,6 +75,13 @@ data class LwaTypography(
val dice: TextStyle,
val result: TextStyle,
)
@Stable
data class Debug(
val title: TextStyle,
val propertyId: SpanStyle,
val propertyValue: TextStyle,
)
}
@Composable
@ -196,7 +205,24 @@ fun lwaTypography(
result = robotoMono.h4.copy(
color = colors.base.onSurface,
),
)
),
debug = LwaTypography.Debug(
title = robotoMono.caption.copy(
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 14.sp,
),
propertyId = robotoMono.caption.copy(
fontWeight = FontWeight.Normal,
fontSize = 10.sp,
lineHeight = 14.sp,
).toSpanStyle(),
propertyValue = robotoMono.caption.copy(
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
lineHeight = 14.sp,
),
),
)
}
}