@file:Suppress("FunctionName") package link.roadio.riff.ui import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp internal data class TopActionSpec( val icon: String, val label: String, val enabled: Boolean = true, val action: PlayerUiAction? = null, val onClick: (() -> Unit)? = null, val enabledAppearance: Boolean? = null, val onLongClick: PlayerUiAction? = null, val onLongClickLabel: String? = null, ) @Composable internal fun RiffTopBar( leading: List, title: String? = null, trailing: TopActionSpec? = null, wide: Boolean, titleAfterLeadingCount: Int? = null, titleContent: (@Composable (Boolean) -> Unit)? = null, ) { CompositionLocalProvider(LocalRiffChromeText provides true) { Box(Modifier.fillMaxWidth().height(uiDimension("topBarHeight")), contentAlignment = Alignment.CenterStart) { RiffTopBarContent(leading, title, trailing, wide, titleAfterLeadingCount, titleContent) } } } @Composable internal fun RiffTopBarContent( leading: List, title: String?, trailing: TopActionSpec?, wide: Boolean, titleAfterLeadingCount: Int?, titleContent: (@Composable (Boolean) -> Unit)?, ) { if (titleContent != null) { val direction = LocalLayoutDirection.current val slot = uiDimension("navTarget") + uiDimension("targetSpacingMin") val minimumWidth = slot * leading.size + uiDimension("navTarget") + if (trailing != null) slot else 0.dp TopBarStrip(minimumWidth, Modifier.testTag("az:browser-chrome")) { stripModifier -> CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { Row(stripModifier.height(uiDimension("navTarget")), verticalAlignment = Alignment.CenterVertically) { CompositionLocalProvider(LocalLayoutDirection provides direction) { leading.forEachIndexed { index, spec -> if (index > 0) Spacer(Modifier.width(uiDimension("targetSpacingMin"))) TopAction(spec, wide) } if (leading.isNotEmpty()) Spacer(Modifier.width(uiDimension("targetSpacingMin"))) Box(Modifier.weight(1f).height(uiDimension("navTarget")).testTag("az:browser-plates")) { titleContent(true) } trailing?.let { Spacer(Modifier.width(uiDimension("targetSpacingMin"))) TopAction(it, wide) } } } } } return } val split = titleAfterLeadingCount?.coerceIn(0, leading.size) val beforeCount = split ?: leading.size val centerPresent = title != null || beforeCount < leading.size val actionCount = (if (wide) leading.size else beforeCount) + if (trailing != null) 1 else 0 val centerWidth = when { wide && split != null && title != null -> 220.dp centerPresent -> uiDimension("navTarget") else -> 0.dp } val gaps = (actionCount - 1 + if (centerPresent) 1 else 0).coerceAtLeast(0) val minimumWidth = uiDimension("navTarget") * actionCount + uiDimension("targetSpacingMin") * gaps + centerWidth TopBarStrip(minimumWidth) { stripModifier -> StandardTopBarContent(leading, title, trailing, wide, split, stripModifier) } } @Composable private fun TopBarStrip( minimumWidth: Dp, modifier: Modifier = Modifier, content: @Composable (Modifier) -> Unit, ) { // AI-INTENT: preserve configured targets even below the fixed-slot width. Scroll the // complete strip only then; ordinary edge anchoring and middle overflow stay intact. // https://wiki.271.pics/Riff/LATEST/ui-foundation#physical-ui-and-artwork val scroll = rememberScrollState() BoxWithConstraints(modifier.fillMaxWidth().height(uiDimension("topBarHeight"))) { content( if (maxWidth < minimumWidth) { Modifier.horizontalScroll(scroll).width(minimumWidth) } else { Modifier.fillMaxWidth() }, ) } } @Composable private fun StandardTopBarContent( leading: List, title: String?, trailing: TopActionSpec?, wide: Boolean, split: Int?, modifier: Modifier, ) { val palette = LocalRiffPalette.current val before = split?.let(leading::take) ?: leading val after = split?.let(leading::drop).orEmpty() if (!wide) { Row( modifier.height(uiDimension("topBarHeight")), verticalAlignment = Alignment.CenterVertically, ) { before.forEachIndexed { index, spec -> if (index > 0) Spacer(Modifier.width(uiDimension("targetSpacingMin"))) TopAction(spec, false) } if (before.isNotEmpty() && (title != null || after.isNotEmpty())) Spacer(Modifier.width(uiDimension("targetSpacingMin"))) // Keep edge actions pinned; only the title/page region may overflow. Row( Modifier.weight(1f).fillMaxHeight().then( if (title != null || after.isNotEmpty()) Modifier.horizontalScroll(rememberScrollState()) else Modifier, ), verticalAlignment = Alignment.CenterVertically, ) { title?.let { RiffText( it, uiTextSize("textTitleList"), palette.text, medium = true, modifier = Modifier.width(220.dp).fillMaxHeight(), maxLines = 1, ) } after.forEach { spec -> Spacer(Modifier.width(uiDimension("targetSpacingMin"))) TopAction(spec, false) } } trailing?.let { Spacer(Modifier.width(uiDimension("targetSpacingMin"))) TopAction(it, false) } } return } Row( modifier.height(uiDimension("topBarHeight")), verticalAlignment = Alignment.CenterVertically, ) { before.forEachIndexed { index, spec -> if (index > 0) Spacer(Modifier.width(uiDimension("targetSpacingMin"))) TopAction(spec, wide) } if (split == null) { if (title != null) { RiffText( title, uiTextSize("textTitleList"), palette.text, medium = true, modifier = Modifier.weight(1f).fillMaxHeight(), textAlign = TextAlign.End, maxLines = 1, ) } else { Spacer(Modifier.weight(1f)) } } else { if (before.isNotEmpty()) Spacer(Modifier.width(uiDimension("targetSpacingMin"))) title?.let { RiffText( it, uiTextSize("textTitleList"), palette.text, medium = true, // Keep later page actions measurable: fillMaxWidth would consume their slots. modifier = Modifier.width(220.dp).fillMaxHeight(), maxLines = 1, ) } after.forEach { spec -> Spacer(Modifier.width(uiDimension("targetSpacingMin"))) TopAction(spec, wide) } Spacer(Modifier.weight(1f)) } trailing?.let { TopAction(it, wide) } } } @Composable internal fun TopAction( spec: TopActionSpec, wide: Boolean, ) { val dispatch = LocalRiffDispatch.current RiffIconButton( spec.icon, spec.label, enabled = spec.enabled, appearanceEnabled = spec.enabledAppearance, wide = wide, contentPadding = BREADCRUMB_BUTTON_CONTENT_PADDING_DP.dp, onClick = { spec.onClick?.invoke() ?: spec.action?.let(dispatch) }, onLongClick = spec.onLongClick?.let { action -> { dispatch(action) } }, onLongClickLabel = spec.onLongClickLabel, modifier = Modifier.size(uiDimension("navTarget")), ) }