@file:Suppress("FunctionName") package link.roadio.riff.ui import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.disabled import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.selected import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.toggleableState import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp // The full allocation is the click/focus target; glyph padding must not shrink it. // AI-INTENT: Keep padding outside the vector viewport so hit/focus geometry never changes. @Composable @Suppress("UNUSED_PARAMETER") internal fun RiffIconButton( icon: String, label: String, modifier: Modifier = Modifier, selected: Boolean = false, enabled: Boolean = true, appearanceEnabled: Boolean? = null, artworkBackdrop: Boolean = false, squareViewport: Boolean = true, contentPadding: Dp = BUTTON_CONTENT_PADDING_DP.dp, wide: Boolean, onClick: () -> Unit, onLongClick: (() -> Unit)? = null, onLongClickLabel: String? = null, text: String? = null, ) { val config = LocalRiffConfig.current val palette = LocalRiffPalette.current val interaction = remember { MutableInteractionSource() } val haptic = LocalHapticFeedback.current val pressed by interaction.collectIsPressedAsState() var focused by remember { mutableStateOf(false) } val tint = when { appearanceEnabled == false -> Color(palette.inactiveIcon) appearanceEnabled == null && !enabled -> Color(palette.disabledIcon) focused -> Color(palette.onPrimary) pressed || selected -> Color(palette.primary) else -> Color(palette.text) } val shape = RoundedCornerShape(12.dp) val description = "$label${if (selected) "; selected" else ""}${if (enabled) "" else "; unavailable"}" val disabledTapBlocker = disabledBackgroundFlipBlocker(enabled) Box( modifier .semantics { contentDescription = description role = Role.Button this.selected = selected if (!enabled) disabled() }.onFocusChanged { focused = it.isFocused } .then(disabledTapBlocker) .combinedClickable( enabled = enabled, interactionSource = interaction, indication = null, role = Role.Button, onClick = onClick, onLongClickLabel = onLongClickLabel, onLongClick = onLongClick?.let { action -> { haptic.performHapticFeedback(HapticFeedbackType.LongPress) action() } }, ).focusable(enabled, interaction), contentAlignment = Alignment.Center, ) { if (enabled && (focused || pressed)) { RiffStateOutline( Modifier.matchParentSize(), Color(palette.focusRing), shape, 3.dp, surround = artworkBackdrop || LocalRiffUncontrolledBackground.current, ) } BoxWithConstraints( Modifier.fillMaxSize().padding(contentPadding), contentAlignment = Alignment.Center, ) { val viewportSide = minOf(maxWidth, maxHeight) if (selected) { SelectedControlCircle( Modifier.size(viewportSide), enabled = enabled, surround = artworkBackdrop || LocalRiffUncontrolledBackground.current, ) } if (text != null) { RiffText( text, config.float("typography.mixedButtonMinFontSize"), tint.toArgb(), monospace = true, bold = true, fixedLabel = true, textAlign = TextAlign.Center, maxLines = 1, softWrap = false, artworkBackdrop = artworkBackdrop, ) } else { RiffIcon( iconDrawableName(icon), tint, squareViewport = squareViewport, modifier = Modifier.fillMaxSize().contrastHalo( if (artworkBackdrop || LocalRiffUncontrolledBackground.current) { if (config.night) Color.Black else Color.White } else { null }, ), ) } } } } @Composable @Suppress("UNUSED_PARAMETER") internal fun ChoiceTarget( description: String, selected: Boolean, enabled: Boolean, wide: Boolean, onClick: () -> Unit, modifier: Modifier, checkbox: Boolean = false, content: @Composable () -> Unit, ) { val palette = LocalRiffPalette.current val interaction = remember { MutableInteractionSource() } val pressed by interaction.collectIsPressedAsState() var focused by remember { mutableStateOf(false) } val shape = RoundedCornerShape(12.dp) val disabledTapBlocker = disabledBackgroundFlipBlocker(enabled) Box( modifier .semantics { contentDescription = description role = if (checkbox) Role.Checkbox else Role.Button if (checkbox) { toggleableState = if (selected) androidx.compose.ui.state.ToggleableState.On else androidx.compose.ui.state.ToggleableState.Off } else { this.selected = selected } if (!enabled) disabled() }.onFocusChanged { focused = it.isFocused } .then(disabledTapBlocker) .clickable( enabled = enabled, interactionSource = interaction, indication = null, role = if (checkbox) Role.Checkbox else Role.Button, onClick = onClick, ).focusable(enabled, interaction), contentAlignment = Alignment.Center, ) { if (enabled && (focused || pressed)) { RiffStateOutline(Modifier.matchParentSize(), Color(palette.focusRing), shape, 3.dp) } BoxWithConstraints( Modifier.fillMaxSize().padding(BUTTON_CONTENT_PADDING_DP.dp), contentAlignment = Alignment.Center, ) { if (selected) { SelectedControlCircle(Modifier.size(minOf(maxWidth, maxHeight)), enabled = enabled) } content() } } } @Composable internal fun RiffControl( description: String, modifier: Modifier = Modifier, enabled: Boolean = true, selected: Boolean = false, onClick: () -> Unit, onLongClick: (() -> Unit)? = null, onLongClickLabel: String? = null, contentPadding: Dp = BUTTON_CONTENT_PADDING_DP.dp, content: @Composable () -> Unit, ) { val config = LocalRiffConfig.current val palette = LocalRiffPalette.current val interaction = remember { MutableInteractionSource() } val pressed by interaction.collectIsPressedAsState() var focused by remember { mutableStateOf(false) } val active = selected || pressed || focused val shape = RoundedCornerShape(config.float("geometry.cornerRadius").dp) val background = Color(if (active) palette.selected else palette.surface) .copy(alpha = config.int("shape.panelAlpha") / 255f) val borderWidth = when { focused || pressed -> 3.dp selected -> config.float("geometry.nowPlayingBorder").dp else -> config.float("shape.panelStrokeDp").dp } val haptic = LocalHapticFeedback.current val disabledTapBlocker = disabledBackgroundFlipBlocker(enabled) val clickModifier = if (onLongClick != null) { Modifier.combinedClickable( enabled = enabled, interactionSource = interaction, indication = null, role = Role.Button, onClick = onClick, onLongClickLabel = onLongClickLabel, onLongClick = { haptic.performHapticFeedback(HapticFeedbackType.LongPress) onLongClick() }, ) } else { Modifier.clickable( enabled = enabled, interactionSource = interaction, indication = null, role = Role.Button, onClick = onClick, ) } Box( modifier .semantics { contentDescription = description role = Role.Button this.selected = selected if (!enabled) disabled() }.onFocusChanged { focused = it.isFocused } .clip(shape) .background(background) .then(disabledTapBlocker) .then(clickModifier) .focusable(enabled, interaction), contentAlignment = Alignment.CenterStart, ) { Box( Modifier.fillMaxSize().padding(contentPadding), contentAlignment = Alignment.CenterStart, ) { CompositionLocalProvider(LocalRiffControlEnabled provides enabled) { content() } } if (enabled && borderWidth > 0.dp) { RiffStateOutline( Modifier.matchParentSize(), Color(if (focused || pressed) palette.focusRing else palette.outline), shape, borderWidth, ) } } }