From 0a9feea59054e4416f7a02e6567b46587d7df35a Mon Sep 17 00:00:00 2001 From: zZebrahz <11633690+zZebrahz@users.noreply.github.com> Date: Wed, 21 Oct 2026 04:10:30 +0000 Subject: [PATCH 1/2] fix(tv): show the sign-in code in large digits without boxes The Android TV sign-in code drew each character in its own small tile, which left the digits too small to read from the couch. Draw the code as one line of 40sp Inter Bold with tabular, slashed-zero digits instead: an en space keeps the 4+4 groups apart, the zero stays distinct from O, and the text shrinks to fit longer server codes. It matches the rendered size of the Apple TV code. Both the QR sign-in screen and the phone pairing panel use the same composable. --- .../tv/ui/components/marquee/TvMarquee.kt | 68 ++++++++----------- 1 file changed, 30 insertions(+), 38 deletions(-) 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..bc5e3a1a0 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 @@ -52,10 +55,12 @@ import androidx.compose.ui.text.font.FontWeight 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 +70,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 +386,32 @@ 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, 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() + // A character is at most about 0.7em wide; fontScale is dp per sp. + val fitSize = maxWidth.value / (text.length.coerceAtLeast(1) * 0.7f * LocalDensity.current.fontScale) + // A code reads left to right in every locale. + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + Text( + text.replace(' ', '\u2002'), + modifier = modifier.clearAndSetSemantics { contentDescription = "Code: " + text.toList().joinToString(", ") }, + color = MarqueeColors.Ink, + fontSize = minOf(40f, fitSize).sp, + fontWeight = FontWeight.Bold, + fontFamily = InterFamily, + maxLines = 1, + softWrap = false, + textAlign = TextAlign.Center, + style = LocalTextStyle.current.copy(fontFeatureSettings = "tnum, zero"), + ) } } From 8f5a5d6cc7d3e196213813204ee3005930fc1ac4 Mon Sep 17 00:00:00 2001 From: zZebrahz <11633690+zZebrahz@users.noreply.github.com> Date: Wed, 21 Oct 2026 04:22:34 +0000 Subject: [PATCH 2/2] fix(tv): measure the sign-in code before sizing it Older servers send letter codes, and in Inter Bold a W or M is about twice as wide as a digit, so a per-character width estimate could pick 40sp for a code that then clipped in the card. Measure the code as drawn and scale it down to fit the width instead. --- .../tv/ui/components/marquee/TvMarquee.kt | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) 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 bc5e3a1a0..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 @@ -52,6 +52,7 @@ 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 @@ -388,30 +389,41 @@ fun TvMarqueeServerCard(name: String, address: String, markUrl: String?, secure: /** * 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, so the text shrinks to keep the row inside the card. Matches Apple - * TV's `MarqueeCodeTiles` in rendered size (72pt there). + * 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 text = code.uppercase() - // A character is at most about 0.7em wide; fontScale is dp per sp. - val fitSize = maxWidth.value / (text.length.coerceAtLeast(1) * 0.7f * LocalDensity.current.fontScale) + 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( - text.replace(' ', '\u2002'), + shown, modifier = modifier.clearAndSetSemantics { contentDescription = "Code: " + text.toList().joinToString(", ") }, - color = MarqueeColors.Ink, - fontSize = minOf(40f, fitSize).sp, - fontWeight = FontWeight.Bold, - fontFamily = InterFamily, maxLines = 1, softWrap = false, - textAlign = TextAlign.Center, - style = LocalTextStyle.current.copy(fontFeatureSettings = "tnum, zero"), + style = style.copy(fontSize = fontSize.sp), ) } }