diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/BlockPlacement.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/BlockPlacement.kt index 265a042..e6f9eab 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/BlockPlacement.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/BlockPlacement.kt @@ -172,7 +172,8 @@ fun BlockTimeLabel( ) { text -> Text( text = text, - style = MaterialTheme.typography.labelSmall, + // Regular weight against the title's medium above it (#219). + style = MaterialTheme.typography.labelSmall.asEventTime(), maxLines = maxLines, overflow = overflow.overflow, softWrap = overflow.softWrap, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventTimeStyle.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventTimeStyle.kt new file mode 100644 index 0000000..85ba7f9 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/EventTimeStyle.kt @@ -0,0 +1,93 @@ +package de.jeanlucmakiola.calendula.ui.common + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.sp +import de.jeanlucmakiola.floret.locale.currentLocale + +/** + * How an event's time is set against its title (#219). + * + * The label styles carry a medium weight and 0.5sp of tracking, which left a + * time reading as part of the title next to it. The time steps down to regular + * and closes the tracking up, so the two read as a time and a title — whether + * they sit on one line, as they do on a wide month chip, or on two, as they do + * in a timed block. + * + * The ink is not dimmed further to do it: at this size a fainter grey would + * give up the contrast [SECONDARY_INK_ALPHA] exists to hold, so the weight + * carries the difference instead. + */ +private val TIME_WEIGHT = FontWeight.Normal +private val TIME_TRACKING = 0.sp + +/** Ink for a title, against [SECONDARY_INK_ALPHA] for the time beside it. */ +const val TITLE_INK_ALPHA = 0.85f + +/** [this] set as an event's time rather than as its title. */ +fun TextStyle.asEventTime(): TextStyle = + copy(fontWeight = TIME_WEIGHT, letterSpacing = TIME_TRACKING) + +/** The same, for a time that shares one line with the title after it. */ +private val TIME_SPAN = SpanStyle(fontWeight = TIME_WEIGHT, letterSpacing = TIME_TRACKING) + +/** Text reading [time], set apart in [timeInk], before [title]. */ +fun inlineTimeLabel(time: String?, title: String, timeInk: Color): AnnotatedString = + buildAnnotatedString { + if (time != null) { + withStyle(TIME_SPAN.copy(color = timeInk)) { append(time) } + append(" ") + } + append(title) + } + +/** + * The characters of title that have to survive a time prefix for it to be worth + * its place — the threshold is a readable title, so it is stated in title + * characters and priced at the surface's own text style rather than guessed in + * dp. Lowercase Latin of average advance: not "iii", which would let the time + * in where nothing else fits, nor "WWW", which would keep it out of a column + * with room to spare. + * + * Seven rather than a rounder eight because the 12-hour convention spends a + * meridiem the 24-hour one does not, and eight would have cost a landscape + * phone the time in 12-hour while granting it in 24-hour. A narrow portrait + * column is nowhere near either figure, so the gate is unchanged where it + * matters most. + */ +private const val TITLE_SAMPLE = "notepad" + +/** The widest wall-clock time in either convention: two-digit hour, and a meridiem in 12-hour. */ +private const val SAMPLE_HOUR = 12 +private const val SAMPLE_MINUTE = 45 + +/** + * The narrowest run of [style] text that may carry a time in front of a title: + * wide enough for the widest time in the current convention plus [TITLE_SAMPLE]. + * + * Measured rather than a device breakpoint, so it follows the font scale, the + * 12/24-hour setting, the locale's own time format and whatever else has + * already been taken off the width — landscape, an unfolded foldable and a + * tablet all come out wide enough without any of them being named. + */ +@Composable +fun rememberInlineTimeWidth(style: TextStyle): Dp { + val measurer = rememberTextMeasurer() + val density = LocalDensity.current + val locale = currentLocale() + val sample = formatTimeOfDay(SAMPLE_HOUR, SAMPLE_MINUTE, LocalUse24HourFormat.current, locale) + return remember(sample, style, density, measurer) { + val label = inlineTimeLabel(sample, TITLE_SAMPLE, Color.Unspecified) + with(density) { measurer.measure(label, style).size.width.toDp() } + } +} diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt index 18b0ad1..93674b3 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt @@ -738,7 +738,8 @@ private fun DragCopy( if (label != null) { Text( text = label, - style = MaterialTheme.typography.labelSmall, + // As the block it lifted off sets its own time (#219). + style = MaterialTheme.typography.labelSmall.asEventTime(), maxLines = 1, overflow = titleOverflow.overflow, softWrap = titleOverflow.softWrap, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthChipTime.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthChipTime.kt index 7163158..175bbe4 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthChipTime.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthChipTime.kt @@ -2,23 +2,11 @@ package de.jeanlucmakiola.calendula.ui.month import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.rememberTextMeasurer -import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import de.jeanlucmakiola.calendula.domain.EventInstance -import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat -import de.jeanlucmakiola.calendula.ui.common.SECONDARY_INK_ALPHA import de.jeanlucmakiola.calendula.ui.common.formatTimeOfDay -import de.jeanlucmakiola.floret.locale.currentLocale +import de.jeanlucmakiola.calendula.ui.common.rememberInlineTimeWidth import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import java.util.Locale @@ -32,56 +20,6 @@ internal val MONTH_CHIP_INSET = CELL_GAP + 1.dp /** Horizontal space a chip spends on chrome rather than on text, both sides. */ private val CHIP_CHROME = (MONTH_CHIP_INSET + MONTH_CHIP_TEXT_PADDING) * 2f -/** - * The seven characters of title a chip has to keep for the time to be worth its - * place (#219) — the threshold is a readable title, so it is stated in title - * characters and priced at the chip's own text style rather than guessed in dp. - * Lowercase Latin of average advance: not "iii", which would let the time in on - * a column where nothing else fits, nor "WWW", which would keep it out of one - * with room to spare. - * - * Seven rather than a rounder eight because the 12-hour convention spends a - * meridiem the 24-hour one does not, and eight would have cost a landscape - * phone the time in 12-hour while granting it in 24-hour. A narrow portrait - * column is nowhere near either figure, so the gate is unchanged where it - * matters most. - */ -private const val TITLE_SAMPLE = "notepad" - -/** The widest wall-clock time in either convention: two-digit hour, plus a meridiem in 12-hour. */ -private const val SAMPLE_HOUR = 12 -private const val SAMPLE_MINUTE = 45 - -/** - * Ink for a chip's title, against [SECONDARY_INK_ALPHA] for the time in front of - * it — the same pairing the week and day blocks put a title and its time label - * in, so a chip that carries both reads the way a block does. - */ -internal const val TITLE_INK_ALPHA = 0.85f - -/** - * How the time is set apart from the title it precedes (#219). labelSmall's - * medium weight steps down to regular and its 0.5sp tracking closes up, so the - * two read as a time and a title rather than as one run-on string. Both moves - * also narrow the prefix, which is worth most on the column that only just - * earned it. - * - * The ink is handed in rather than dimmed further: at this size a fainter grey - * would give up the contrast [SECONDARY_INK_ALPHA] exists to hold, so the - * weight carries the difference instead. - */ -private val TIME_SPAN = SpanStyle(fontWeight = FontWeight.Normal, letterSpacing = 0.sp) - -/** A chip's text — its [time], set apart in [timeInk], before the [title]. */ -internal fun monthChipLabel(time: String?, title: String, timeInk: Color): AnnotatedString = - buildAnnotatedString { - if (time != null) { - withStyle(TIME_SPAN.copy(color = timeInk)) { append(time) } - append(" ") - } - append(title) - } - /** * The start time a chip shows before its title (#219), or null when it has none * to show: an all-day chip never carries a time, and neither does a bar carried @@ -99,25 +37,7 @@ internal fun monthChipTime( return formatTimeOfDay(start.hour, start.minute, is24Hour, locale) } -/** - * The narrowest chip that may show a time (#219): wide enough for the widest - * time in the current convention plus [TITLE_SAMPLE]'s worth of title. - * - * Measured rather than a device breakpoint, so it follows the font scale, the - * 12/24-hour setting, the locale's own time format and the week-number gutter - * on its own — landscape, an unfolded foldable and a tablet all come out wide - * enough without any of them being named. - */ +/** The narrowest chip that may show a time — the text it needs, plus its chrome. */ @Composable -internal fun rememberMonthTimeChipWidth(): Dp { - val measurer = rememberTextMeasurer() - val style = MaterialTheme.typography.labelSmall - val density = LocalDensity.current - val locale = currentLocale() - val sample = formatTimeOfDay(SAMPLE_HOUR, SAMPLE_MINUTE, LocalUse24HourFormat.current, locale) - return remember(sample, style, density, measurer) { - val label = monthChipLabel(sample, TITLE_SAMPLE, Color.Unspecified) - val textPx = measurer.measure(label, style).size.width - with(density) { textPx.toDp() } + CHIP_CHROME - } -} +internal fun rememberMonthTimeChipWidth(): Dp = + rememberInlineTimeWidth(MaterialTheme.typography.labelSmall) + CHIP_CHROME diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt index 90bd503..5922b83 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt @@ -148,6 +148,8 @@ import de.jeanlucmakiola.calendula.ui.common.LocalDimCutoff import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat import de.jeanlucmakiola.calendula.ui.common.SECONDARY_INK_ALPHA +import de.jeanlucmakiola.calendula.ui.common.TITLE_INK_ALPHA +import de.jeanlucmakiola.calendula.ui.common.inlineTimeLabel import de.jeanlucmakiola.calendula.ui.common.eventAccent import de.jeanlucmakiola.calendula.ui.common.EventChipShape import de.jeanlucmakiola.calendula.ui.common.monthBarShape @@ -2478,7 +2480,7 @@ private fun MonthBar( val fill = eventFill(event.color, dark, soften) // The same title/secondary ink pairing the week and day blocks use, with // the time on the quieter half. - val label = monthChipLabel(time, title, eventInk(fill, alpha = SECONDARY_INK_ALPHA)) + val label = inlineTimeLabel(time, title, eventInk(fill, alpha = SECONDARY_INK_ALPHA)) val moveAction = eventMoveAction(event) // The source stays put as a ghost while its floating copy travels. val monthDrag = LocalMonthDrag.current diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/ui/month/MonthChipTimeTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/ui/month/MonthChipTimeTest.kt index 21698d6..bccea31 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/ui/month/MonthChipTimeTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/ui/month/MonthChipTimeTest.kt @@ -3,6 +3,7 @@ package de.jeanlucmakiola.calendula.ui.month import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import com.google.common.truth.Truth.assertThat +import de.jeanlucmakiola.calendula.ui.common.inlineTimeLabel import de.jeanlucmakiola.calendula.domain.EventInstance import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.LocalDate @@ -59,7 +60,7 @@ class MonthChipTimeTest { @Test fun `the time is set apart from the title it precedes`() { - val label = monthChipLabel("09:05", "Standup", Color.Black) + val label = inlineTimeLabel("09:05", "Standup", Color.Black) assertThat(label.text).isEqualTo("09:05 Standup") // Only the time is restyled: the title keeps labelSmall as the theme // sets it, so the two read as separate things on one line (#219). @@ -72,7 +73,7 @@ class MonthChipTimeTest { @Test fun `a chip without a time is styled title and nothing else`() { - val label = monthChipLabel(null, "Standup", Color.Black) + val label = inlineTimeLabel(null, "Standup", Color.Black) assertThat(label.text).isEqualTo("Standup") assertThat(label.spanStyles).isEmpty() }