diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRow.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRow.kt index 31a92e5..315c91c 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRow.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRow.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource @@ -177,8 +178,14 @@ internal data class RowTimeStyle(val style: TextStyle, val maxWidth: Dp) { private const val FitMargin = 0.95f -/** Wide enough for "12:59:59 PM" at the default scale; the city keeps the rest. */ -private val SecondsMaxWidth = 168.dp +/** + * The least the time may take: wide enough for "12:59:59 PM" at the default + * scale. On a wider window the cap grows with it, to [SecondsMaxShare] of the + * window, so a larger font scale is honoured wherever there is room for it. + */ +private val SecondsMinCap = 168.dp + +private const val SecondsMaxShare = 0.45f /** * Wide times in every format: a two-digit hour, eights, and each day period, @@ -189,7 +196,7 @@ private val WidestSamples: List = listOf(LocalTime.of(10, 58, 58), Lo /** * Without seconds, the row's time exactly as it always was: `headlineMedium`, * bold, uncapped. With seconds, a size down, and shrunk further only if the - * widest sample would not fit [SecondsMaxWidth], so at a large font scale the + * widest sample would not fit the cap, so at a large font scale the * time cannot take the whole row and leave the city no width at all. * Measured once per format, style and density, never per tick: the digits are * tabular, so no time is wider than the samples. @@ -199,13 +206,15 @@ internal fun rememberRowTimeStyle(showSeconds: Boolean, formatTime: (LocalTime) val base = if (showSeconds) MaterialTheme.typography.headlineSmall else MaterialTheme.typography.headlineMedium val measurer = rememberTextMeasurer(cacheSize = 0) val density = LocalDensity.current - return remember(showSeconds, base, formatTime, density) { + val windowWidth = LocalConfiguration.current.screenWidthDp.dp + val maxWidth = maxOf(SecondsMinCap, windowWidth * SecondsMaxShare) + return remember(showSeconds, base, formatTime, density, measurer, maxWidth) { val style = base.copy(fontWeight = FontWeight.Bold) if (!showSeconds) return@remember RowTimeStyle(style, Dp.Infinity) val width = WidestSamples.maxOf { sample -> measurer.measure(formatTime(sample), style, maxLines = 1, softWrap = false).size.width } - val cap = with(density) { SecondsMaxWidth.toPx() } + val cap = with(density) { maxWidth.toPx() } // Through px and back, not by scaling the sp value: from Android 14 // large font scales are non-linear, so sp and px are not proportional. val fitted = if (width <= cap) { @@ -214,7 +223,7 @@ internal fun rememberRowTimeStyle(showSeconds: Boolean, formatTime: (LocalTime) // A few percent spare: hinted glyph advances do not scale exactly. style.copy(fontSize = with(density) { (style.fontSize.toPx() * FitMargin * cap / width).toSp() }) } - RowTimeStyle(fitted, SecondsMaxWidth) + RowTimeStyle(fitted, maxWidth) } } diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockScreen.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockScreen.kt index 2fcba37..b4efa20 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockScreen.kt @@ -147,7 +147,9 @@ fun WorldClockScreen( modifier = Modifier.fillMaxWidth(), rowHeight = 80.dp, ) { id, position, dragHandle, isDragging -> - val row = rowsById.getValue(id) + // Always present while the column re-seeds in composition; + // drawn as nothing rather than crashing if that ever lags. + val row = rowsById[id] ?: return@ReorderableColumn WorldClockRow( row = row, position = position, diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockSource.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockSource.kt index f250453..d38424b 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockSource.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockSource.kt @@ -46,17 +46,17 @@ internal class WorldClockSource @Inject constructor( * **every tick**. [ClockChanges] restarts the tick, so `TIME_SET` and * `TIMEZONE_CHANGED` show at once rather than at the next minute, and the * restarted ticker measures its next wait against the clock as it now - * reads. It sits - * outside the `flatMapLatest`, so its receiver is registered once per - * subscription rather than once per list edit, and `onStart` lets the - * first state through without waiting for a broadcast. [refreshes] does - * the same for what no broadcast announces: the screen sends one on every - * resume, which covers a per-app language change (the activity is - * recreated, the ViewModel and this flow are not) and a `delay` stretched - * by a deep sleep (the main looper's clock stops while the device sleeps) - * without waiting for the next minute. + * reads. It sits outside the `flatMapLatest`, so its receiver is + * registered once per subscription rather than once per list edit, and + * `onStart` lets the first state through without waiting for a broadcast. * `SystemEventReceiver` still needs no new branch (D2). * + * [refreshes] does the same for what no broadcast announces: the screen + * sends one on every resume, which covers a per-app language change (the + * activity is recreated, the ViewModel and this flow are not) and a + * `delay` stretched by a deep sleep (the main looper's clock stops while + * the device sleeps) without waiting for the next minute. + * * The tick is aligned to the wall clock and only as fine as the readouts: * once a minute, or once a second when the user turned seconds on (#23). * Everything shown is truncated to that same unit, so each tick is one new