Add the alteration page in the GameMaster screen.

This commit is contained in:
Andres Gomez, Thomas (ITDV RL) 2025-03-28 15:03:53 +01:00
parent ee4445490c
commit 76336dfbb0
17 changed files with 507 additions and 44 deletions

View file

@ -242,5 +242,7 @@
<string name="game_master__character_action__add_to_npc">Ajouter aux Npcs</string>
<string name="game_master__character_action__remove_from_npc">Retirer des Npcs</string>
<string name="game_master__create_character_sheet">Créer un personnage</string>
<string name="game_master__alteration__filter">Filtrer par nom :</string>
<string name="game_master__alteration__delete">Supprimer l'altération</string>
</resources>

View file

@ -36,6 +36,8 @@ import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.CharacterSheetEdi
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.common.SkillFieldFactory
import com.pixelized.desktop.lwa.ui.screen.gamemaster.GameMasterViewModel
import com.pixelized.desktop.lwa.ui.screen.gamemaster.action.GMActionViewModel
import com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.GMAlterationFactory
import com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.GMAlterationViewModel
import com.pixelized.desktop.lwa.ui.screen.gamemaster.character.GMCharacterFactory
import com.pixelized.desktop.lwa.ui.screen.gamemaster.character.GMCharacterViewModel
import com.pixelized.desktop.lwa.ui.screen.levelup.LevelUpFactory
@ -122,7 +124,7 @@ val factoryDependencies
factoryOf(::TextMessageFactory)
factoryOf(::LevelUpFactory)
factoryOf(::GMCharacterFactory)
factoryOf(::GMActionViewModel)
factoryOf(::GMAlterationFactory)
}
val viewModelDependencies
@ -145,6 +147,8 @@ val viewModelDependencies
viewModelOf(::PortraitOverlayViewModel)
viewModelOf(::GMCharacterViewModel)
viewModelOf(::GameMasterViewModel)
viewModelOf(::GMActionViewModel)
viewModelOf(::GMAlterationViewModel)
}
val useCaseDependencies

View file

@ -24,6 +24,8 @@ class AlterationRepository(
) {
private val scope = CoroutineScope(Dispatchers.IO + Job())
val alterationFlow get() = alterationStore.alterationsFlow
/**
* This flow transform the campaign instance (player + npc) into a
* Map<CharacterSheetId, List<AlterationId>>.

View file

@ -3,6 +3,7 @@ package com.pixelized.desktop.lwa.ui.navigation.screen.destination.gamemaster
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration.GMAlterationPage
object GMAlterationDestination {
private const val ROUTE = "GameMasterAlteration"
@ -15,7 +16,7 @@ fun NavGraphBuilder.composableGameMasterAlterationPage() {
composable(
route = GMAlterationDestination.baseRoute(),
) {
GMAlterationPage()
}
}

View file

@ -0,0 +1,61 @@
package com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMAlterationUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagItemUio
import com.pixelized.desktop.lwa.utils.extention.unAccent
import com.pixelized.shared.lwa.model.alteration.Alteration
import java.text.Collator
class GMAlterationFactory {
fun filterAlteration(
alterations: Collection<Alteration>,
unAccentFilter: String,
selectedTagId: String?,
): List<Alteration> {
return alterations.filter {
val matchName = it.metadata.name.unAccent().contains(
other = unAccentFilter,
ignoreCase = true
)
val matchTag = selectedTagId == null || it.tags.contains(
element = selectedTagId
)
matchName && matchTag
}
}
fun convertToGMAlterationUio(
alterations: List<Alteration>,
selectedTagId: String?,
): List<GMAlterationUio> {
return alterations
.map { alteration ->
GMAlterationUio(
alterationId = alteration.id,
label = alteration.metadata.name,
tags = alteration.tags.map { tag ->
GMTagItemUio(
id = tag,
label = tag,
highlight = tag == selectedTagId,
)
}
)
}
.sortedWith(compareBy(Collator.getInstance()) { it.label })
}
fun convertToGMTagItemUio(
alterationTagIds: List<String>,
selectedTagId: String?,
): List<GMTagItemUio> {
return alterationTagIds.map {
GMTagItemUio(
id = it,
label = it,
highlight = it == selectedTagId,
)
}
}
}

View file

@ -0,0 +1,144 @@
package com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Button
import androidx.compose.material.Icon
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMAlteration
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMAlterationUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMFilterHeader
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagItemUio
import com.pixelized.desktop.lwa.ui.theme.color.component.LwaButtonColors
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.game_master__create_character_sheet
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
@Composable
fun GMAlterationPage(
viewModel: GMAlterationViewModel = koinViewModel(),
) {
val alterations = viewModel.alterations.collectAsState()
val tags = viewModel.alterationTags.collectAsState()
Box {
GMAlterationContent(
modifier = Modifier.fillMaxSize(),
filter = viewModel.filter,
tags = tags,
alterations = alterations,
onTag = viewModel::onTag,
onAlterationEdit = { },
onAlterationDelete = { },
onAlterationCreate = { },
)
}
}
@Composable
private fun GMAlterationContent(
modifier: Modifier = Modifier,
padding: Dp = 8.dp,
spacing: Dp = 8.dp,
filter: LwaTextFieldUio,
tags: State<List<GMTagItemUio>>,
alterations: State<List<GMAlterationUio>>,
onTag: (String) -> Unit,
onAlterationEdit: (String) -> Unit,
onAlterationDelete: (String) -> Unit,
onAlterationCreate: () -> Unit,
) {
Column(
modifier = modifier,
) {
Surface(
elevation = 1.dp,
) {
GMFilterHeader(
padding = padding,
spacing = spacing,
filter = filter,
tags = tags,
onTag = onTag,
)
}
Box(
modifier = Modifier.fillMaxWidth().weight(1f),
) {
LazyColumn(
modifier = Modifier.matchParentSize(),
contentPadding = remember {
PaddingValues(
start = padding,
top = padding,
end = padding,
bottom = padding + 48.dp + padding,
)
},
verticalArrangement = Arrangement.spacedBy(space = spacing),
) {
items(
items = alterations.value,
key = { it.alterationId },
) { alteration ->
GMAlteration(
modifier = Modifier
.fillMaxWidth()
.animateItem(),
alteration = alteration,
onAlteration = {
onAlterationEdit(alteration.alterationId)
},
onDelete = {
onAlterationDelete(alteration.alterationId)
},
onTag = onTag,
)
}
}
Row(
modifier = Modifier
.align(alignment = Alignment.BottomEnd)
.padding(all = padding),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Button(
colors = LwaButtonColors(),
shape = CircleShape,
onClick = onAlterationCreate,
) {
Text(
modifier = Modifier.padding(end = 4.dp),
text = stringResource(Res.string.game_master__create_character_sheet),
)
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
)
}
}
}
}
}

View file

@ -0,0 +1,89 @@
package com.pixelized.desktop.lwa.ui.screen.gamemaster.alteration
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.desktop.lwa.repository.alteration.AlterationRepository
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
import com.pixelized.desktop.lwa.utils.extention.unAccent
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.runBlocking
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.game_master__character__filter
import org.jetbrains.compose.resources.getString
class GMAlterationViewModel(
alterationRepository: AlterationRepository,
alterationFactory: GMAlterationFactory,
) : ViewModel() {
private val _filter = MutableStateFlow("")
val filter = LwaTextFieldUio(
enable = true,
labelFlow = MutableStateFlow(runBlocking { getString(Res.string.game_master__character__filter) }),
valueFlow = _filter,
isError = MutableStateFlow(false),
placeHolderFlow = MutableStateFlow(null),
onValueChange = { _filter.value = it },
)
@OptIn(ExperimentalCoroutinesApi::class)
private val alterationTagIds = alterationRepository.alterationFlow
.mapLatest { alterations -> alterations.values.flatMap { it.tags }.toSet().toList() }
.distinctUntilChanged()
private val selectedTagId = MutableStateFlow<String?>(null)
val alterationTags = combine(
alterationTagIds,
selectedTagId,
) { alterationTagIds, selectedTagId ->
alterationFactory.convertToGMTagItemUio(
alterationTagIds = alterationTagIds,
selectedTagId = selectedTagId,
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = emptyList(),
)
@OptIn(ExperimentalCoroutinesApi::class)
val alterations = combine(
alterationRepository.alterationFlow,
filter.valueFlow.map { it.unAccent() },
selectedTagId,
transform = { alterations, unAccentFilter, selectedTagId ->
alterationFactory.filterAlteration(
alterations = alterations.values,
unAccentFilter = unAccentFilter,
selectedTagId = selectedTagId
)
}
).mapLatest { alterations ->
alterationFactory.convertToGMAlterationUio(
alterations = alterations,
selectedTagId = selectedTagId.value
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = emptyList(),
)
fun onTag(id: String) {
selectedTagId.update {
when (it) {
id -> null
else -> id
}
}
}
}

View file

@ -13,11 +13,16 @@ import org.jetbrains.compose.resources.getString
class GMCharacterFactory {
companion object {
const val PLAYER_ID = "PLAYER"
const val NPC_ID = "NPC"
}
suspend fun convertToGMCharacterPreviewUio(
campaign: Campaign,
characters: List<CharacterSheetPreview>,
filter: String,
tags: Map<GMTagItemUio.TagId, Boolean>,
tags: Map<String, Boolean>,
): List<GMCharacterItemUio> {
val normalizedFilter = filter.unAccent()
@ -35,7 +40,7 @@ class GMCharacterFactory {
campaign: Campaign,
character: CharacterSheetPreview,
filter: String,
tags: Map<GMTagItemUio.TagId, Boolean>,
tags: Map<String, Boolean>,
): GMCharacterItemUio? {
// get the characterInstanceId from the player list corresponding to this CharacterSheet if any
val isPlayer = campaign.characters.firstOrNull {
@ -55,11 +60,11 @@ class GMCharacterFactory {
}
}
// Tag filter process : Player.
if (tags[GMTagItemUio.TagId.PLAYER] == true && isPlayer.not()) {
if (tags[PLAYER_ID] == true && isPlayer.not()) {
return null
}
// Tag filter process : Npc.
if (tags[GMTagItemUio.TagId.NPC] == true && isNpc.not()) {
if (tags[NPC_ID] == true && isNpc.not()) {
return null
}
// Build the call tag list.
@ -67,18 +72,18 @@ class GMCharacterFactory {
if (isPlayer) {
add(
GMTagItemUio(
id = GMTagItemUio.TagId.PLAYER,
id = PLAYER_ID,
label = getString(Res.string.game_master__character_tag__character),
highlight = tags[GMTagItemUio.TagId.PLAYER] ?: false,
highlight = tags[PLAYER_ID] ?: false,
)
)
}
if (isNpc) {
add(
GMTagItemUio(
id = GMTagItemUio.TagId.NPC,
id = NPC_ID,
label = getString(Res.string.game_master__character_tag__npc),
highlight = tags[GMTagItemUio.TagId.NPC] ?: false,
highlight = tags[NPC_ID] ?: false,
)
)
}
@ -104,4 +109,25 @@ class GMCharacterFactory {
actions = actions,
)
}
suspend fun convertToGMTagItemUio(
id: String?,
highlight: Boolean,
): GMTagItemUio? {
return when (id) {
PLAYER_ID -> GMTagItemUio(
id = id,
label = getString(Res.string.game_master__character_tag__character),
highlight = highlight,
)
NPC_ID -> GMTagItemUio(
id = id,
label = getString(Res.string.game_master__character_tag__npc),
highlight = highlight,
)
else -> null
}
}
}

View file

@ -165,7 +165,7 @@ fun GMCharacterContent(
filter: LwaTextFieldUio,
tags: State<List<GMTagItemUio>>,
characters: State<List<GMCharacterItemUio>>,
onTag: (GMTagItemUio.TagId) -> Unit,
onTag: (String) -> Unit,
onCharacterAction: (String, GMCharacterItemUio.Action) -> Unit,
onCharacterSheetDetail: (String) -> Unit,
onCharacterSheetEdit: (String) -> Unit,

View file

@ -5,11 +5,9 @@ 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 com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterItemUio.Action
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagItemUio
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagItemUio.TagId
import com.pixelized.shared.lwa.protocol.websocket.GameMasterEvent
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@ -41,22 +39,15 @@ class GMCharacterViewModel(
onValueChange = { _filter.value = it },
)
private val _tags = MutableStateFlow(mapOf(TagId.PLAYER to false, TagId.NPC to false))
val tags = _tags.map { it: Map<TagId, Boolean> ->
it.map { (tag, highlight) ->
when (tag) {
TagId.PLAYER -> GMTagItemUio(
id = TagId.PLAYER,
label = getString(Res.string.game_master__character_tag__character),
highlight = highlight,
)
TagId.NPC -> GMTagItemUio(
id = TagId.NPC,
label = getString(Res.string.game_master__character_tag__npc),
highlight = highlight,
)
}
private val _tags = MutableStateFlow(
mapOf(
GMCharacterFactory.PLAYER_ID to false,
GMCharacterFactory.NPC_ID to false
)
)
val tags = _tags.map { it: Map<String, Boolean> ->
it.mapNotNull { (id, highlight) ->
factory.convertToGMTagItemUio(id = id, highlight = highlight)
}
}.stateIn(
scope = viewModelScope,
@ -113,7 +104,7 @@ class GMCharacterViewModel(
}
fun onTag(
id: TagId,
id: String,
) {
_tags.value = _tags.value.toMutableMap().also {
it[id] = it.getOrPut(id) { true }.not()

View file

@ -0,0 +1,142 @@
package com.pixelized.desktop.lwa.ui.screen.gamemaster.items
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.theme.lwa
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.game_master__alteration__delete
import lwacharactersheet.composeapp.generated.resources.ic_delete_forever_24dp
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
@Stable
data class GMAlterationUio(
val alterationId: String,
val label: String,
val tags: List<GMTagItemUio>,
)
@Stable
object GMAlterationDefault {
val padding = PaddingValues(start = 16.dp)
}
@Composable
fun GMAlteration(
modifier: Modifier = Modifier,
padding: PaddingValues = GMAlterationDefault.padding,
alteration: GMAlterationUio,
onAlteration: () -> Unit,
onDelete: () -> Unit,
onTag: (String) -> Unit,
) {
Row(
modifier = Modifier
.clip(shape = MaterialTheme.lwa.shapes.gameMaster)
.clickable(onClick = onAlteration)
.background(color = MaterialTheme.lwa.colorScheme.elevated.base1dp)
.minimumInteractiveComponentSize()
.padding(paddingValues = padding)
.then(other = modifier),
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
style = MaterialTheme.lwa.typography.base.body1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = alteration.label,
)
Row(
modifier = Modifier.weight(1f).height(intrinsicSize = IntrinsicSize.Min),
horizontalArrangement = Arrangement.spacedBy(space = 2.dp, alignment = Alignment.End)
) {
alteration.tags.forEach { tag ->
GMTag(
elevation = 4.dp,
tag = tag,
onTag = { onTag(tag.id) },
)
}
}
OverflowActionMenu(
alteration = alteration,
onDelete = onDelete,
)
}
}
@Composable
private fun OverflowActionMenu(
modifier: Modifier = Modifier,
alteration: GMAlterationUio,
onDelete: () -> Unit,
) {
val overflowMenu = remember(alteration) {
mutableStateOf(false)
}
IconButton(
modifier = modifier,
onClick = { overflowMenu.value = true },
) {
Icon(
imageVector = Icons.Default.MoreVert,
tint = MaterialTheme.colors.primary,
contentDescription = null,
)
DropdownMenu(
expanded = overflowMenu.value,
onDismissRequest = {
overflowMenu.value = false
},
content = {
DropdownMenuItem(
onClick = {
overflowMenu.value = false
onDelete()
},
) {
Row(
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(Res.drawable.ic_delete_forever_24dp),
tint = MaterialTheme.lwa.colorScheme.base.primary,
contentDescription = null,
)
Text(
style = MaterialTheme.lwa.typography.base.body1,
color = MaterialTheme.lwa.colorScheme.base.primary,
text = stringResource(Res.string.game_master__alteration__delete),
)
}
}
},
)
}
}

View file

@ -19,6 +19,7 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
@ -94,7 +95,7 @@ data class GMCharacterItemUio(
}
object GMCharacterPreviewDefault {
val padding = PaddingValues(horizontal = 16.dp)
val padding = PaddingValues(start = 16.dp)
}
@OptIn(ExperimentalFoundationApi::class)
@ -106,11 +107,8 @@ fun GMCharacter(
onClick: () -> Unit,
onSecondary: () -> Unit,
onAction: (Action) -> Unit,
onTag: (GMTagItemUio.TagId) -> Unit,
onTag: (String) -> Unit,
) {
val layoutDirection = LocalLayoutDirection.current
val startPadding = padding.calculateStartPadding(layoutDirection)
Box(
modifier = Modifier
.clip(shape = MaterialTheme.lwa.shapes.gameMaster)
@ -120,10 +118,11 @@ fun GMCharacter(
)
.clickable(onClick = onClick)
.background(color = MaterialTheme.lwa.colorScheme.elevated.base1dp)
.minimumInteractiveComponentSize()
.padding(paddingValues = padding)
.then(other = modifier),
) {
Row(
modifier = Modifier.padding(start = startPadding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {

View file

@ -42,7 +42,7 @@ fun GMFilterHeader(
spacing: Dp = 8.dp,
filter: LwaTextFieldUio,
tags: State<List<GMTagItemUio>>,
onTag: (GMTagItemUio.TagId) -> Unit,
onTag: (String) -> Unit,
) {
val scope = rememberCoroutineScope()

View file

@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.theme.lwa
@ -19,15 +20,10 @@ import com.pixelized.desktop.lwa.ui.theme.lwa
@Stable
data class GMTagItemUio(
val id: TagId,
val id: String,
val label: String,
val highlight: Boolean,
) {
@Stable
enum class TagId {
PLAYER, NPC
}
}
)
@Stable
object GmTagDefault {
@ -60,6 +56,8 @@ fun GMTag(
.padding(paddingValues = padding),
style = MaterialTheme.lwa.typography.base.caption,
color = animatedColor.value,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = tag.label,
)
}