Pith - kiln
package com.vgmlr.kiln

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextOverflow
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.unit.sp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupPositionProvider
import androidx.compose.ui.window.PopupProperties
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.foundation.border

internal val TOP_BAR_H = 64.dp
internal val BAR_GAP = 8.dp
internal val BAR_INSET_H = 12.dp
private val BAR_INSET_V = 8.dp
private val BAR_CONTENT_GAP = 8.dp

private val BTN_SIZE = 48.dp
private val BTN_RADIUS = 6.dp
private val BTN_ICON = 26.dp
private val BTN_PAD = 11.dp
private const val BTN_ALPHA = 1.0f
private const val BTN_FADE = 0.35f
private val BTN_LABEL_PAD = BTN_PAD + 4.dp
private val BTN_LABEL = 18.sp
private val BlankBg = Color(0xFF272A30)
private val PlaceholderFg = TopBarFg.copy(alpha = 0.4f)

private const val DEPLOY_MS = 180
private val DEPLOY_SPEC = tween<Float>(DEPLOY_MS, easing = FastOutSlowInEasing)

private val BelowAnchor = object : PopupPositionProvider {
    override fun calculatePosition(
        anchorBounds: IntRect,
        windowSize: IntSize,
        layoutDirection: LayoutDirection,
        popupContentSize: IntSize
    ) = IntOffset(
        anchorBounds.left + (anchorBounds.width - popupContentSize.width) / 2,
        anchorBounds.bottom
    )
}

internal class KilnDeployItem(
    val icon: ImageVector,
    val desc: String,
    val iconPlus: Dp = 0.dp,
    val enabled: Boolean = true,
    val onClick: () -> Unit
)

@Composable
internal fun KilnBarButton(
    icon: ImageVector,
    desc: String?,
    modifier: Modifier = Modifier,
    iconSize: Dp = BTN_ICON,
    iconPad: Dp = BTN_PAD,
    enabled: Boolean = true,
    onClick: () -> Unit
) {
    Box(
        modifier = modifier
            .size(BTN_SIZE)
            .clip(RoundedCornerShape(ROUND_END))
            .background(TopBarBg)
            .clickable(enabled = enabled, onClick = onClick)
            .padding(iconPad),
        contentAlignment = Alignment.Center
    ) {
        Icon(
            icon,
            desc,
            Modifier.size(iconSize),
            tint = TopBarFg.copy(alpha = if (enabled) BTN_ALPHA else BTN_FADE)
        )
    }
}

@Composable
internal fun KilnBlankButton(
    modifier: Modifier = Modifier,
    label: String? = null,
    labelAlignment: Alignment = Alignment.CenterEnd
) {
    Box(
        modifier = modifier
            .size(BTN_SIZE)
            .clip(RoundedCornerShape(ROUND_END))
            .background(BlankBg)
            .border(EDGE, RoundedCornerShape(ROUND_END))
            .padding(horizontal = BTN_LABEL_PAD),
        contentAlignment = labelAlignment
    ) {
        if (label != null) Text(
            label,
            color = TopBarFg,
            fontSize = BTN_LABEL,
            maxLines = 1,
            overflow = TextOverflow.Ellipsis
        )
    }
}

@Composable
internal fun KilnBlankField(
    value: String,
    onValueChange: (String) -> Unit,
    modifier: Modifier = Modifier,
    placeholder: String = "Unnamed"
) {
    val style = TextStyle(color = TopBarFg, fontSize = BTN_LABEL, textAlign = TextAlign.End)
    Box(
        modifier = modifier
            .size(BTN_SIZE)
            .clip(RoundedCornerShape(ROUND_END))
            .background(BlankBg)
            .border(EDGE, RoundedCornerShape(ROUND_END))
            .padding(horizontal = BTN_LABEL_PAD),
        contentAlignment = Alignment.CenterEnd
    ) {
        BasicTextField(
            value = value,
            onValueChange = onValueChange,
            singleLine = true,
            textStyle = style,
            cursorBrush = SolidColor(TopBarFg),
            modifier = Modifier.fillMaxWidth(),
            decorationBox = { inner ->
                Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
                    if (value.isEmpty()) Text(
                        placeholder,
                        style = style.copy(color = PlaceholderFg),
                        maxLines = 1
                    )
                    inner()
                }
            }
        )
    }
}

@Composable
internal fun KilnBackButton(onBack: () -> Unit, modifier: Modifier = Modifier) {
    KilnBarButton(
        Icons.AutoMirrored.Filled.ArrowBack,
        "Back",
        modifier.statusBarsPadding().padding(start = BAR_INSET_H, top = BAR_INSET_V),
        onClick = onBack
    )
}

@Composable
internal fun KilnBarSpacer() {
    Spacer(Modifier.statusBarsPadding().height(TOP_BAR_H + BAR_CONTENT_GAP))
}

@Composable
internal fun KilnDeploy(
    expanded: Boolean,
    onDismiss: () -> Unit,
    items: List<KilnDeployItem>
) {
    val p = remember { Animatable(0f) }
    LaunchedEffect(expanded) { p.animateTo(if (expanded) 1f else 0f, DEPLOY_SPEC) }
    if (!expanded && p.value == 0f) return

    val step = with(LocalDensity.current) { (BTN_SIZE + BAR_GAP).toPx() }
    Popup(
        popupPositionProvider = BelowAnchor,
        onDismissRequest = onDismiss,
        properties = PopupProperties(focusable = true)
    ) {
        Column(
            Modifier.padding(top = BAR_GAP),
            verticalArrangement = Arrangement.spacedBy(BAR_GAP)
        ) {
            items.forEachIndexed { i, item ->
                KilnBarButton(
                    item.icon,
                    item.desc,
                    Modifier.graphicsLayer {
                        translationY = -(i + 1) * step * (1f - p.value)
                        alpha = p.value
                    },
                    iconSize = BTN_ICON + item.iconPlus,
                    iconPad = BTN_PAD - item.iconPlus / 2,
                    enabled = item.enabled,
                    onClick = item.onClick
                )
            }
        }
    }
}