package com.vgmlr.stave
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
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.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextAlign
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.BorderStroke
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 BAR_GAP = 8.dp
internal val BAR_EDGE = 12.dp
internal val BAR_EDGE_ACTIONS = 8.dp
internal val DOCK_PAD = 16.dp
private val TITLE_FS = 18.sp
private val BLANK_BG = Color(0xFF272A30)
internal val EDGE = BorderStroke(1.dp, Color(0xFF868D97).copy(alpha = 0.30f))
internal const val UNNAMED = "Unnamed"
private val BelowAnchorEnd = object : PopupPositionProvider {
override fun calculatePosition(
anchorBounds: IntRect,
windowSize: IntSize,
layoutDirection: LayoutDirection,
popupContentSize: IntSize
) = IntOffset(anchorBounds.right - popupContentSize.width, anchorBounds.bottom)
}
@Composable
internal fun StaveBlankButton(
modifier: Modifier = Modifier,
label: String? = null,
labelAlignment: Alignment = Alignment.CenterEnd,
placeholder: String = "",
onRename: ((String) -> Unit)? = null
) {
val ink = colorResource(id = R.color.title_color)
Box(
modifier = modifier
.height(BTN_SIZE)
.defaultMinSize(minWidth = BTN_SIZE)
.clip(RoundedCornerShape(ROUND_SET))
.background(BLANK_BG)
.border(EDGE, RoundedCornerShape(ROUND_SET))
.padding(horizontal = BTN_PAD + 4.dp),
contentAlignment = labelAlignment
) {
if (onRename != null) {
TitleField(label.orEmpty(), placeholder, ink, labelAlignment, onRename)
} else if (label != null) {
Text(
label,
color = ink,
fontSize = TITLE_FS,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
@Composable
private fun TitleField(
value: String,
placeholder: String,
ink: Color,
alignment: Alignment,
onCommit: (String) -> Unit
) {
var text by remember(value) { mutableStateOf(value) }
val focus = LocalFocusManager.current
val style = TextStyle(
color = ink,
fontSize = TITLE_FS,
textAlign = if (alignment == Alignment.CenterStart) TextAlign.Start else TextAlign.End
)
BasicTextField(
value = text,
onValueChange = { text = it },
singleLine = true,
textStyle = style,
cursorBrush = SolidColor(ink),
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Sentences,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { focus.clearFocus() }),
modifier = Modifier
.fillMaxWidth()
.onFocusChanged { if (!it.isFocused && text.trim() != value) onCommit(text.trim()) },
decorationBox = { inner ->
Box(contentAlignment = alignment) {
if (text.isEmpty()) {
Text(placeholder, style = style.copy(color = ink.copy(alpha = 0.5f)), maxLines = 1)
}
inner()
}
}
)
}
@Composable
internal fun StaveButton(
icon: ImageVector,
desc: String?,
modifier: Modifier = Modifier,
iconSize: Dp = BTN_ICON,
iconPad: Dp = BTN_PAD,
enabled: Boolean = true,
bg: Color? = null,
onClick: () -> Unit
) {
Box(
modifier = modifier
.height(BTN_SIZE)
.widthIn(min = BTN_SIZE)
.clip(RoundedCornerShape(ROUND_SET))
.background(bg ?: colorResource(id = R.color.primary_color))
.clickable(enabled = enabled, onClick = onClick)
.padding(iconPad),
contentAlignment = Alignment.Center
) {
Icon(
icon,
contentDescription = desc,
modifier = Modifier.size(iconSize),
tint = colorResource(id = R.color.title_color)
.copy(alpha = if (enabled) 1f else 0.3f)
)
}
}
@Composable
internal fun StaveMenu(
expanded: Boolean,
onDismiss: () -> Unit,
side: (@Composable ColumnScope.() -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit
) {
if (!expanded) return
Popup(
popupPositionProvider = BelowAnchorEnd,
onDismissRequest = onDismiss,
properties = PopupProperties(focusable = true)
) {
Row(
modifier = Modifier.padding(top = BAR_GAP),
horizontalArrangement = Arrangement.spacedBy(BAR_GAP),
verticalAlignment = Alignment.Top
) {
if (side != null) {
Column(verticalArrangement = Arrangement.spacedBy(BAR_GAP), content = side)
}
Column(verticalArrangement = Arrangement.spacedBy(BAR_GAP), content = content)
}
}
}
@Composable
internal fun StaveMenuItem(
label: String,
icon: ImageVector,
enabled: Boolean = true,
iconSize: Dp = BTN_ICON,
bg: Color? = null,
onClick: () -> Unit
) = StaveButton(
icon,
label,
iconSize = iconSize,
iconPad = BTN_PAD - (iconSize - BTN_ICON) / 2,
enabled = enabled,
bg = bg,
onClick = onClick
)
@Composable
internal fun BoxScope.StaveDock(content: @Composable RowScope.() -> Unit) {
Row(
Modifier
.align(Alignment.BottomCenter)
.navigationBarsPadding()
.padding(bottom = DOCK_PAD),
horizontalArrangement = Arrangement.spacedBy(BAR_GAP),
verticalAlignment = Alignment.CenterVertically,
content = content
)
}