package dev.darkpyonix.composerust.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.ui.Alignment as ComposeAlignment
import androidx.compose.ui.text.style.TextAlign as ComposeTextAlign
import androidx.compose.ui.text.style.TextOverflow as ComposeOverflow
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dev.darkpyonix.composerust.protocol.Alignment
import dev.darkpyonix.composerust.protocol.Arrangement as ProtocolArrangement
import dev.darkpyonix.composerust.protocol.ButtonVariant
import dev.darkpyonix.composerust.protocol.ColorRole
import dev.darkpyonix.composerust.protocol.Paint
import dev.darkpyonix.composerust.protocol.PropertyKind
import dev.darkpyonix.composerust.protocol.PropertyValue
import dev.darkpyonix.composerust.protocol.SpaceRole
import dev.darkpyonix.composerust.protocol.TextAlign
import dev.darkpyonix.composerust.protocol.TextOverflow
import dev.darkpyonix.composerust.protocol.TypeRole
import dev.darkpyonix.composerust.design.ResolvedTheme
import dev.darkpyonix.composerust.ui.node.Node
import dev.darkpyonix.composerust.design.composeLetterSpacing
import dev.darkpyonix.composerust.design.composeLineHeight
import dev.darkpyonix.composerust.design.composeWeight
import dev.darkpyonix.composerust.design.fontSize
/**
* Reads design properties off a node and turns them into Compose values.
*
* Every role arrives as its wire tag in an integer property, and tag 0 is reserved for
* "not sent", so an absent role is simply a missing property rather than a guessed default.
*/
internal fun <T : Enum<T>> Node.role(kind: PropertyKind, values: Array<T>): T? {
val tag = (props[kind] as? PropertyValue.Integer)?.value ?: return null
val index = tag.toInt() - 1
return values.getOrNull(index)
}
internal fun Node.floatProp(kind: PropertyKind): Float? =
when (val value = props[kind]) {
is PropertyValue.Float -> value.value.takeIf { it != 0f }
is PropertyValue.Integer -> value.value.toFloat().takeIf { it != 0f }
else -> null
}
internal fun Node.intProp(kind: PropertyKind): Long? =
(props[kind] as? PropertyValue.Integer)?.value?.takeIf { it != 0L }
/** The `Color` property carries `Paint` bits, like every other colour slot. */
internal fun Node.paintProp(kind: PropertyKind): Paint? {
val bits = intProp(kind) ?: return null
val value = bits.toInt()
return when ((bits ushr 32).toInt()) {
1 -> ColorRole.entries.getOrNull(value - 1)?.let { Paint.Role(it) }
2 -> Paint.Literal(value)
else -> null
}
}
internal fun Node.typeRole(): TypeRole? = role(PropertyKind.TypeRole, TypeRole.entries.toTypedArray())
internal fun Node.variant(): ButtonVariant =
role(PropertyKind.Variant, ButtonVariant.entries.toTypedArray()) ?: ButtonVariant.Filled
/**
* The text style for a node: the design system's rung of the ladder, with each override
* replacing exactly one axis.
*/
internal fun Node.textStyle(theme: ResolvedTheme, defaultRole: TypeRole = TypeRole.Body): TextStyle {
val role = typeRole() ?: defaultRole
val token = theme.type(role)
val paint = paintProp(PropertyKind.Color)
return TextStyle(
color = if (paint != null) theme.color(paint) else theme.color(ColorRole.OnSurface),
fontSize = floatProp(PropertyKind.FontSize)?.sp ?: token.fontSize,
fontWeight = intProp(PropertyKind.FontWeight)
?.let { androidx.compose.ui.text.font.FontWeight(it.toInt()) }
?: token.composeWeight,
lineHeight = floatProp(PropertyKind.LineHeight)?.sp ?: token.composeLineHeight,
letterSpacing = floatProp(PropertyKind.LetterSpacing)?.sp ?: token.composeLetterSpacing,
fontFamily = theme.family(role),
textAlign = when (role(PropertyKind.TextAlign, TextAlign.entries.toTypedArray())) {
TextAlign.Start -> ComposeTextAlign.Start
TextAlign.Center -> ComposeTextAlign.Center
TextAlign.End -> ComposeTextAlign.End
TextAlign.Justify -> ComposeTextAlign.Justify
null -> ComposeTextAlign.Unspecified
},
)
}
internal fun Node.maxLines(): Int = intProp(PropertyKind.MaxLines)?.toInt() ?: Int.MAX_VALUE
internal fun Node.overflow(): ComposeOverflow =
when (role(PropertyKind.Overflow, TextOverflow.entries.toTypedArray())) {
TextOverflow.Clip -> ComposeOverflow.Clip
TextOverflow.Ellipsis -> ComposeOverflow.Ellipsis
TextOverflow.Visible -> ComposeOverflow.Visible
null -> ComposeOverflow.Clip
}
/** `spacing` is a literal dp, `space_role` goes through the table; the literal wins. */
internal fun Node.spacing(theme: ResolvedTheme): Dp {
floatProp(PropertyKind.Spacing)?.let { return it.dp }
val spaceRole = role(PropertyKind.SpaceRole, SpaceRole.entries.toTypedArray()) ?: return 0.dp
return theme.space(spaceRole)
}
private fun Node.arrangementRole(): ProtocolArrangement? =
role(PropertyKind.Arrangement, ProtocolArrangement.entries.toTypedArray())
internal fun Node.verticalArrangement(theme: ResolvedTheme): Arrangement.Vertical {
val space = spacing(theme)
val arrangement = arrangementRole()
if (space.value > 0f) {
return Arrangement.spacedBy(
space,
when (arrangement) {
ProtocolArrangement.Center -> ComposeAlignment.CenterVertically
ProtocolArrangement.End -> ComposeAlignment.Bottom
else -> ComposeAlignment.Top
},
)
}
return when (arrangement) {
ProtocolArrangement.Center -> Arrangement.Center
ProtocolArrangement.End -> Arrangement.Bottom
ProtocolArrangement.SpaceBetween -> Arrangement.SpaceBetween
ProtocolArrangement.SpaceAround -> Arrangement.SpaceAround
ProtocolArrangement.SpaceEvenly -> Arrangement.SpaceEvenly
ProtocolArrangement.Start, null -> Arrangement.Top
}
}
internal fun Node.horizontalArrangement(theme: ResolvedTheme): Arrangement.Horizontal {
val space = spacing(theme)
val arrangement = arrangementRole()
if (space.value > 0f) {
return Arrangement.spacedBy(
space,
when (arrangement) {
ProtocolArrangement.Center -> ComposeAlignment.CenterHorizontally
ProtocolArrangement.End -> ComposeAlignment.End
else -> ComposeAlignment.Start
},
)
}
return when (arrangement) {
ProtocolArrangement.Center -> Arrangement.Center
ProtocolArrangement.End -> Arrangement.End
ProtocolArrangement.SpaceBetween -> Arrangement.SpaceBetween
ProtocolArrangement.SpaceAround -> Arrangement.SpaceAround
ProtocolArrangement.SpaceEvenly -> Arrangement.SpaceEvenly
ProtocolArrangement.Start, null -> Arrangement.Start
}
}
private fun Node.alignmentRole(): Alignment? =
role(PropertyKind.Alignment, Alignment.entries.toTypedArray())
/** A Column aligns its children on the horizontal axis. */
internal fun Node.horizontalAlignment(): ComposeAlignment.Horizontal =
when (alignmentRole()) {
Alignment.TopCenter, Alignment.Center, Alignment.BottomCenter ->
ComposeAlignment.CenterHorizontally
Alignment.TopEnd, Alignment.CenterEnd, Alignment.BottomEnd -> ComposeAlignment.End
else -> ComposeAlignment.Start
}
/** A Row aligns its children on the vertical axis. */
internal fun Node.verticalAlignment(): ComposeAlignment.Vertical =
when (alignmentRole()) {
Alignment.CenterStart, Alignment.Center, Alignment.CenterEnd ->
ComposeAlignment.CenterVertically
Alignment.BottomStart, Alignment.BottomCenter, Alignment.BottomEnd -> ComposeAlignment.Bottom
else -> ComposeAlignment.Top
}
/** A Box takes the full nine point alignment. */
internal fun Node.boxAlignment(): ComposeAlignment = when (alignmentRole()) {
Alignment.TopStart, null -> ComposeAlignment.TopStart
Alignment.TopCenter -> ComposeAlignment.TopCenter
Alignment.TopEnd -> ComposeAlignment.TopEnd
Alignment.CenterStart -> ComposeAlignment.CenterStart
Alignment.Center -> ComposeAlignment.Center
Alignment.CenterEnd -> ComposeAlignment.CenterEnd
Alignment.BottomStart -> ComposeAlignment.BottomStart
Alignment.BottomCenter -> ComposeAlignment.BottomCenter
Alignment.BottomEnd -> ComposeAlignment.BottomEnd
}