compose-rust 0.0.1

Declarative UI in Rust, rendered by an AOT-compiled Compose Multiplatform engine.
Documentation
package dev.darkpyonix.composerust.design

import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import dev.darkpyonix.composerust.protocol.ColorRole
import dev.darkpyonix.composerust.protocol.Theme
import kotlin.math.pow

/**
 * Every fill whose ink this side may choose, with the contrast that pair is held to.
 *
 * The same list the Host checks a palette against, in the same order. Accents hold 3:1,
 * the bound a label on a control is held to; containers and the reading surfaces hold
 * 4.5:1, because a paragraph lands on them.
 */
internal val INK_PAIRS: List<Triple<ColorRole, ColorRole, Float>> = listOf(
    Triple(ColorRole.Primary, ColorRole.OnPrimary, 3.0f),
    Triple(ColorRole.Secondary, ColorRole.OnSecondary, 3.0f),
    Triple(ColorRole.Tertiary, ColorRole.OnTertiary, 3.0f),
    Triple(ColorRole.Error, ColorRole.OnError, 3.0f),
    Triple(ColorRole.PrimaryContainer, ColorRole.OnPrimaryContainer, 4.5f),
    Triple(ColorRole.SecondaryContainer, ColorRole.OnSecondaryContainer, 4.5f),
    Triple(ColorRole.TertiaryContainer, ColorRole.OnTertiaryContainer, 4.5f),
    Triple(ColorRole.Surface, ColorRole.OnSurface, 4.5f),
    Triple(ColorRole.SurfaceVariant, ColorRole.OnSurfaceVariant, 4.5f),
    Triple(ColorRole.Background, ColorRole.OnBackground, 4.5f),
)

private fun channel(value: Int): Double {
    val v = value / 255.0
    return if (v <= 0.04045) v / 12.92 else ((v + 0.055) / 1.055).pow(2.4)
}

/** Relative luminance as WCAG defines it, from the colour's eight bit channels. */
internal fun wcagLuminance(color: Color): Double {
    val argb = color.toArgb()
    return 0.2126 * channel((argb shr 16) and 0xff) +
        0.7152 * channel((argb shr 8) and 0xff) +
        0.0722 * channel(argb and 0xff)
}

/**
 * The ink for a fill the application changed and whose ink it left alone.
 *
 * The system's own ink where it still reads at the bound the pair is held to; otherwise
 * white or black, whichever reads better. A brand orange keeps the system's white where
 * white reads on it, and a yellow brand gets black.
 */
internal fun inkFor(fill: Color, systemInk: Color, required: Float): Color {
    if (contrastRatio(systemInk, fill) >= required) return systemInk
    return if (contrastRatio(Color.White, fill) >= contrastRatio(Color.Black, fill)) {
        Color.White
    } else {
        Color.Black
    }
}

/**
 * The colours the application's palette puts over a design system, for one scheme,
 * indexed by role ordinal, with null where the system's own value stands.
 *
 * Read from what the theme already carries, so a switch between light and dark is
 * answered here without a word to the Host. A fill given without its ink also gets an ink
 * here, chosen against [system]'s answer for that ink; the other direction is never
 * filled in, because an ink given on its own was meant.
 *
 * Empty while the platform is in a high contrast mode. The person using the machine chose
 * those colours to be able to see, and a brand must not paint over them.
 */
internal fun applicationColors(
    theme: Theme?,
    dark: Boolean,
    highContrast: () -> Boolean,
    system: (ColorRole) -> Color,
): List<Color?> {
    // Asked last, so that an application with no palette never makes the platform answer.
    if (theme == null || theme.palette.isEmpty() || highContrast()) return emptyList()
    val given = arrayOfNulls<Color>(ColorRole.entries.size)
    for (entry in theme.palette) {
        if (entry.dark == dark) given[entry.role.ordinal] = Color(entry.argb)
    }
    if (given.all { it == null }) return emptyList()
    for ((fill, ink, required) in INK_PAIRS) {
        val newFill = given[fill.ordinal] ?: continue
        if (given[ink.ordinal] != null) continue
        given[ink.ordinal] = inkFor(newFill, system(ink), required)
    }
    return given.toList()
}