compose-rust 0.0.1

Declarative UI in Rust, rendered by an AOT-compiled Compose Multiplatform engine.
Documentation
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
}