diff --git a/androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/components/marquee/TvMarquee.kt b/androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/components/marquee/TvMarquee.kt index 4a2c539b0..ad03a6ee7 100644 --- a/androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/components/marquee/TvMarquee.kt +++ b/androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/components/marquee/TvMarquee.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape 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 @@ -42,6 +43,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.clearAndSetSemantics @@ -49,13 +52,16 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.rememberTextMeasurer 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.LayoutDirection import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.tv.material3.Icon +import androidx.tv.material3.LocalTextStyle import androidx.tv.material3.Text import androidx.compose.material.icons.outlined.PhoneAndroid import androidx.compose.material3.CircularProgressIndicator @@ -65,6 +71,7 @@ import org.siloserver.silo.common.ui.marquee.MarqueeScrimStyle import org.siloserver.silo.common.ui.marquee.MarqueeServerMark import org.siloserver.silo.common.ui.marquee.rememberReduceMotion import org.siloserver.silo.tv.R +import org.siloserver.silo.tv.ui.theme.InterFamily /** * First-run sizes on Android TV: the Apple TV values at half scale, since @@ -380,46 +387,43 @@ fun TvMarqueeServerCard(name: String, address: String, markUrl: String?, secure: } /** - * The code a person compares between the TV and their phone. Codes are - * server-generated and may be longer than eight characters, so tiles shrink - * to keep the row inside the card. + * The code a person compares between the TV and their phone, large enough to + * read from the couch. Plain text, no boxes: the space in `4821 7730` widens + * to an en space to keep the groups apart. Inter's tabular, slashed-zero + * digits keep the columns even and a 0 from passing for an O in older + * servers' letter codes; the system monospace font has no slashed zero. Codes + * are server-generated and may be longer or wider, so the text shrinks to keep + * the row inside the card. Matches Apple TV's `MarqueeCodeTiles` in rendered + * size (72pt there). */ @Composable fun TvMarqueeCodeTiles(code: String, modifier: Modifier = Modifier, maxWidth: Dp = 260.dp) { - val characters = code.uppercase().toList() - val gap = 7.dp - val tileWidth = minOf(42.dp, (maxWidth - gap * (characters.size - 1).coerceAtLeast(0)) / characters.size.coerceAtLeast(1)) - Row( - modifier.clearAndSetSemantics { contentDescription = "Code: " + characters.joinToString(", ") }, - horizontalArrangement = Arrangement.spacedBy(gap), - ) { - characters.forEach { character -> - val separator = character == '-' || character == ' ' - Box( - Modifier - .width(if (separator) tileWidth * 0.45f else tileWidth) - .height(tileWidth * 1.24f) - .then( - if (separator) { - Modifier - } else { - Modifier - .clip(RoundedCornerShape(8.dp)) - .background(MarqueeColors.GlassFill) - .border(1.dp, Color.White.copy(alpha = 0.12f), RoundedCornerShape(8.dp)) - }, - ), - contentAlignment = Alignment.Center, - ) { - Text( - if (character == '-') "–" else character.toString(), - color = if (separator) MarqueeColors.InkTertiary else MarqueeColors.Ink, - fontSize = (tileWidth.value * 0.66f).sp, - fontWeight = FontWeight.Bold, - fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace, - textAlign = TextAlign.Center, - ) - } - } + val text = code.uppercase() + val shown = text.replace(' ', '\u2002') + val style = LocalTextStyle.current.copy( + color = MarqueeColors.Ink, + fontSize = 40.sp, + fontWeight = FontWeight.Bold, + fontFamily = InterFamily, + fontFeatureSettings = "tnum, zero", + textAlign = TextAlign.Center, + ) + // Letters such as W and M are much wider than digits, so measure the code + // as drawn and scale it down to fit. + val measurer = rememberTextMeasurer() + val widthPx = with(LocalDensity.current) { maxWidth.toPx() } + val fontSize = remember(shown, style, widthPx) { + val measured = measurer.measure(shown, style, softWrap = false, maxLines = 1).size.width + if (measured <= widthPx) 40f else 40f * widthPx / measured + } + // A code reads left to right in every locale. + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + Text( + shown, + modifier = modifier.clearAndSetSemantics { contentDescription = "Code: " + text.toList().joinToString(", ") }, + maxLines = 1, + softWrap = false, + style = style.copy(fontSize = fontSize.sp), + ) } }