fix(worldclock): leave a fitting margin and drop the zone-cache reset

The fitted seconds size keeps five percent spare, since hinted glyph
advances do not scale exactly. The receiver no longer resets the
process's default zone: that was speculative, and it changed global
state from a UI flow. The settings state takes the seconds flag without
a default, and a garbled KDoc sentence is fixed.
This commit is contained in:
makiolaj committed 2026-10-21 19:48:30 +02:00
1 parent 6d235e0974
commit 78da94d775
5 files changed
+9 -13

No files matched your search

@@ -10,7 +10,6 @@ import de.jeanlucmakiola.clockula.domain.time.ClockChanges
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import java.util.TimeZone
import javax.inject.Inject
import javax.inject.Singleton
@@ -28,11 +27,6 @@ class AndroidClockChanges @Inject constructor(
override fun changes(): Flow<Unit> = callbackFlow {
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// The process's cached default zone is reset by a separate
// one-way call that can land after this broadcast; resetting it
// here too (as ActivityThread does) means the rebuild this
// triggers reads the new zone, not the old one.
if (intent.action == Intent.ACTION_TIMEZONE_CHANGED) TimeZone.setDefault(null)
trySend(Unit)
}
}
@@ -15,7 +15,7 @@ internal sealed interface SettingsUiState {
val defaults: ClockDefaults,
val selfCheckProblemCount: Int? = null,
/** The world clock's "Display time with seconds" (#23); not a clock default. */
val worldClockSeconds: Boolean = false,
val worldClockSeconds: Boolean,
) : SettingsUiState
}
@@ -175,6 +175,8 @@ internal data class RowTimeStyle(val style: TextStyle, val maxWidth: Dp) {
val capped: Boolean get() = maxWidth != Dp.Infinity
}
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
@@ -209,7 +211,8 @@ internal fun rememberRowTimeStyle(showSeconds: Boolean, formatTime: (LocalTime)
val fitted = if (width <= cap) {
style
} else {
style.copy(fontSize = with(density) { (style.fontSize.toPx() * cap / width).toSp() })
// 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)
}
@@ -45,8 +45,9 @@ internal class WorldClockSource @Inject constructor(
/**
* The tab's state. Both the instant and the device zone are re-read on
* **every tick**. [ClockChanges] restarts the tick, so `TIME_SET` and
* `TIMEZONE_CHANGED` show at once rather than at the next minute, the next
* wait is measured against the clock as it now reads. It sits
* `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
+1 -3
View File
@@ -2464,9 +2464,7 @@ arrive with the tab on screen (a network zone change on landing, split
screen). So `ClockChanges` (`AndroidClockChanges`, a receiver registered only
while the tab collects) restarts the tick: those two make the state re-read
the clock and the zone at once, and the restarted ticker measures its next
wait against the clock as it now reads. On `TIMEZONE_CHANGED` the receiver
also resets the process's cached default zone itself, since the system's own
reset is a separate call that can land after the broadcast. The signal is combined
wait against the clock as it now reads. The signal is combined
with the stored list, the home zone and the setting *outside* the
`flatMapLatest`, so the receiver is registered once per subscription, not
once per list edit. Each broadcast costs one extra `build`, which