feat: tabbed character sheet with 4 sections (Closes #14) (#59)

This commit was merged in pull request #59.
This commit is contained in:
2026-03-13 13:31:54 +01:00
parent 7a32e4da2a
commit 49410fc2db
2 changed files with 193 additions and 19 deletions

View File

@@ -26,11 +26,7 @@ import kotlinx.coroutines.launch
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.shahondin1624.lib.components.TestTags
import org.shahondin1624.lib.components.UiConstants
import org.shahondin1624.lib.components.charactermodel.CharacterHeader
import org.shahondin1624.lib.components.charactermodel.DamageMonitorPanel
import org.shahondin1624.lib.components.charactermodel.DerivedAttributesPanel
import org.shahondin1624.lib.components.charactermodel.ResourcePanel
import org.shahondin1624.lib.components.charactermodel.attributespage.AttributesPage
import org.shahondin1624.lib.components.charactermodel.CharacterSheetPage
import org.shahondin1624.lib.components.settings.SettingsPage
import org.shahondin1624.model.EXAMPLE_CHARACTER
import org.shahondin1624.navigation.AppRoutes
@@ -305,20 +301,7 @@ private fun MainScaffold(
.padding(contentPadding)
) {
composable(AppRoutes.CHARACTER_SHEET) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
CharacterHeader(EXAMPLE_CHARACTER.characterData)
Spacer(Modifier.height(contentPadding))
ResourcePanel(EXAMPLE_CHARACTER.characterData, EXAMPLE_CHARACTER.attributes.edge)
Spacer(Modifier.height(contentPadding))
DerivedAttributesPanel(EXAMPLE_CHARACTER.attributes)
Spacer(Modifier.height(contentPadding))
DamageMonitorPanel(EXAMPLE_CHARACTER.damageMonitor)
Spacer(Modifier.height(contentPadding))
AttributesPage(EXAMPLE_CHARACTER)
}
CharacterSheetPage(EXAMPLE_CHARACTER, contentPadding)
}
composable(AppRoutes.SETTINGS) {
SettingsPage()

View File

@@ -0,0 +1,191 @@
package org.shahondin1624.lib.components.charactermodel
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import org.shahondin1624.lib.components.UiConstants
import org.shahondin1624.lib.components.charactermodel.attributespage.Attribute
import org.shahondin1624.lib.components.charactermodel.attributespage.Talent
import org.shahondin1624.model.charactermodel.ShadowrunCharacter
import org.shahondin1624.theme.LocalWindowSizeClass
import org.shahondin1624.theme.WindowSizeClass
/**
* Tab definitions for the character sheet sections.
*/
private enum class CharacterTab(val title: String) {
Overview("Overview"),
Attributes("Attributes"),
Talents("Talents"),
Combat("Combat")
}
/**
* Tabbed character sheet page that organizes content into logical sections.
* Replaces the previous single-scroll layout with fast tab switching.
*/
@Composable
fun CharacterSheetPage(character: ShadowrunCharacter, contentPadding: Dp) {
val windowSizeClass = LocalWindowSizeClass.current
var selectedTab by remember { mutableStateOf(CharacterTab.Overview) }
val spacing = UiConstants.Spacing.medium(windowSizeClass)
Column(modifier = Modifier.fillMaxSize()) {
// Tab row
TabRow(
selectedTabIndex = selectedTab.ordinal
) {
CharacterTab.entries.forEach { tab ->
Tab(
selected = selectedTab == tab,
onClick = { selectedTab = tab },
text = { Text(tab.title) }
)
}
}
// Tab content
when (windowSizeClass) {
WindowSizeClass.Expanded -> {
// Expanded: two-column layout for Overview and Combat
when (selectedTab) {
CharacterTab.Overview -> ExpandedOverviewContent(character, spacing)
CharacterTab.Attributes -> AttributesContent(character, spacing)
CharacterTab.Talents -> TalentsContent(character, spacing)
CharacterTab.Combat -> CombatContent(character, spacing)
}
}
else -> {
when (selectedTab) {
CharacterTab.Overview -> OverviewContent(character, spacing)
CharacterTab.Attributes -> AttributesContent(character, spacing)
CharacterTab.Talents -> TalentsContent(character, spacing)
CharacterTab.Combat -> CombatContent(character, spacing)
}
}
}
}
}
@Composable
private fun OverviewContent(character: ShadowrunCharacter, spacing: Dp) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(spacing),
verticalArrangement = Arrangement.spacedBy(spacing)
) {
CharacterHeader(character.characterData)
ResourcePanel(character.characterData, character.attributes.edge)
DerivedAttributesPanel(character.attributes)
}
}
@Composable
private fun ExpandedOverviewContent(character: ShadowrunCharacter, spacing: Dp) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(spacing),
verticalArrangement = Arrangement.spacedBy(spacing)
) {
CharacterHeader(character.characterData)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(spacing)
) {
Column(modifier = Modifier.weight(1f)) {
ResourcePanel(character.characterData, character.attributes.edge)
}
Column(modifier = Modifier.weight(1f)) {
DerivedAttributesPanel(character.attributes)
}
}
}
}
@Composable
private fun AttributesContent(character: ShadowrunCharacter, spacing: Dp) {
val windowSizeClass = LocalWindowSizeClass.current
val columns = UiConstants.Grid.totalColumns(windowSizeClass)
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(spacing),
verticalArrangement = Arrangement.spacedBy(spacing)
) {
val attributes = character.attributes.getAllAttributes()
val rows = attributes.chunked(columns)
for (row in rows) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(spacing)
) {
for (attr in row) {
Box(modifier = Modifier.weight(1f)) {
Attribute(attr)
}
}
repeat(columns - row.size) {
Spacer(Modifier.weight(1f))
}
}
}
}
}
@Composable
private fun TalentsContent(character: ShadowrunCharacter, spacing: Dp) {
val windowSizeClass = LocalWindowSizeClass.current
val totalCols = UiConstants.Grid.totalColumns(windowSizeClass)
val talentSpan = UiConstants.Grid.talentSpan(windowSizeClass)
val talentsPerRow = totalCols / talentSpan
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(spacing),
verticalArrangement = Arrangement.spacedBy(spacing)
) {
val talents = character.talents.talents
val rows = talents.chunked(talentsPerRow)
for (row in rows) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(spacing)
) {
for (talent in row) {
Box(modifier = Modifier.weight(1f)) {
Talent(talent, character.attributes)
}
}
repeat(talentsPerRow - row.size) {
Spacer(Modifier.weight(1f))
}
}
}
}
}
@Composable
private fun CombatContent(character: ShadowrunCharacter, spacing: Dp) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(spacing),
verticalArrangement = Arrangement.spacedBy(spacing)
) {
DamageMonitorPanel(character.damageMonitor)
}
}