Pith - kerf
package com.vgmlr.kerf

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Reorder
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupPositionProvider
import androidx.compose.ui.window.PopupProperties
import androidx.compose.foundation.border

internal val TOP_BAR_H = 64.dp
internal val BTN_SIZE = 48.dp
internal val BTN_RADIUS = 6.dp
internal val BTN_ICON = 26.dp
internal val BTN_PAD = 11.dp
internal val BTN_ICON_MENU = 32.dp
internal val BTN_PAD_MENU = (BTN_SIZE - BTN_ICON_MENU) / 2
internal const val BTN_ALPHA = 1.0f
internal val BAR_GAP = 8.dp
internal val ROUND_END = 6.dp
internal val BAR_EDGE = 12.dp
internal val BAR_EDGE_ACTIONS = 8.dp
private val DEPLOY_PAD = 8.dp

private fun deployBelow(gap: Int, pad: Int) = object : PopupPositionProvider {
    override fun calculatePosition(
        anchorBounds: IntRect,
        windowSize: IntSize,
        layoutDirection: LayoutDirection,
        popupContentSize: IntSize
    ) = IntOffset(anchorBounds.right - popupContentSize.width + pad, anchorBounds.bottom + gap - pad)
}

@Composable
internal fun KerfBlankButton(
    modifier: Modifier = Modifier,
    label: String? = null,
    labelAlignment: Alignment = Alignment.CenterEnd,
    content: (@Composable BoxScope.() -> Unit)? = null
) {
    val kerfColors = LocalKerfColors.current!!
    Box(
        modifier = modifier
            .size(BTN_SIZE)
            .clip(RoundedCornerShape(ROUND_END))
            .background(kerfColors.blankTopbar)
            .border(EDGE, RoundedCornerShape(ROUND_END))
            .padding(horizontal = BTN_PAD + 4.dp),
        contentAlignment = labelAlignment
    ) {
        if (content != null) {
            content()
        } else if (label != null) {
            Text(
                label,
                color = MaterialTheme.colorScheme.onPrimary,
                fontSize = KerfDimens.TextSizeTitle,
                maxLines = 1
            )
        }
    }
}

@Composable
internal fun KerfButton(
    icon: ImageVector,
    desc: String?,
    modifier: Modifier = Modifier,
    iconSize: Dp = BTN_ICON,
    iconPad: Dp = BTN_PAD,
    enabled: Boolean = true,
    alpha: Float = BTN_ALPHA,
    bg: Color? = null,
    iconModifier: Modifier = Modifier,
    onClick: () -> Unit
) {
    val kerfColors = LocalKerfColors.current!!
    Box(
        modifier = modifier
            .height(BTN_SIZE)
            .widthIn(min = BTN_SIZE)
            .graphicsLayer(alpha = alpha)
            .clip(RoundedCornerShape(ROUND_END))
            .background(bg ?: kerfColors.appTopbar)
            .clickable(enabled = enabled, onClick = onClick)
            .padding(iconPad),
        contentAlignment = Alignment.Center
    ) {
        Icon(
            icon,
            contentDescription = desc,
            modifier = iconModifier.size(iconSize),
            tint = MaterialTheme.colorScheme.onPrimary.copy(alpha = if (enabled) 1f else 0.3f)
        )
    }
}

@Composable
internal fun KerfMenuTrigger(onClick: () -> Unit) {
    KerfButton(
        Icons.Default.Reorder,
        "Menu",
        iconSize = BTN_ICON_MENU,
        iconPad = BTN_PAD_MENU,
        onClick = onClick
    )
}

internal class KerfDeployItem(
    val icon: ImageVector,
    val desc: String?,
    val bg: Color? = null,
    val enabled: Boolean = true,
    val iconSize: Dp = BTN_ICON,
    val iconPad: Dp = BTN_PAD,
    val onClick: () -> Unit
)

private const val DEPLOY_MS = 180

@Composable
internal fun KerfDeploy(
    expanded: Boolean,
    onDismiss: () -> Unit,
    items: List<KerfDeployItem>,
    side: List<KerfDeployItem> = emptyList(),
    sideSlots: Int = 2
) {
    val progress = remember { Animatable(0f) }
    var shown by remember { mutableStateOf(false) }
    LaunchedEffect(expanded) {
        if (expanded) shown = true
        progress.animateTo(if (expanded) 1f else 0f, tween(DEPLOY_MS))
        if (!expanded) shown = false
    }
    if (!expanded && !shown) return

    val density = LocalDensity.current
    val gapPx = with(density) { BAR_GAP.roundToPx() }
    val padPx = with(density) { DEPLOY_PAD.roundToPx() }
    val stepPx = with(density) { (BTN_SIZE + BAR_GAP).toPx() }
    val position = remember(gapPx, padPx) { deployBelow(gapPx, padPx) }
    val dismiss by rememberUpdatedState(onDismiss)

    Popup(
        popupPositionProvider = position,
        onDismissRequest = onDismiss,
        properties = PopupProperties(focusable = true)
    ) {
        Row(
            // gap taps
            modifier = Modifier
                .pointerInput(Unit) { detectTapGestures { dismiss() } }
                .padding(DEPLOY_PAD),
            horizontalArrangement = Arrangement.spacedBy(BAR_GAP)
        ) {
            if (side.isNotEmpty()) {
                DeployColumn(side, { progress.value }, stepPx)
                // skipped slots
                repeat(sideSlots - 1) { Spacer(Modifier.width(BTN_SIZE)) }
            }
            DeployColumn(items, { progress.value }, stepPx)
        }
    }
}

@Composable
private fun DeployColumn(items: List<KerfDeployItem>, progress: () -> Float, stepPx: Float) {
    Column(verticalArrangement = Arrangement.spacedBy(BAR_GAP)) {
        items.forEachIndexed { i, item ->
            KerfButton(
                item.icon,
                item.desc,
                modifier = Modifier
                    .graphicsLayer {
                        val p = progress()
                        translationY = -(i + 1) * stepPx * (1f - p)
                        alpha = p
                    },
                iconSize = item.iconSize,
                iconPad = item.iconPad,
                enabled = item.enabled,
                bg = item.bg,
                onClick = item.onClick
            )
        }
    }
}