From 065afdb2b892e2edb4fae0725eac0eb42cca388f Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 4 Sep 2026 11:56:36 +0200 Subject: [PATCH] Set the month chip time apart from its title (#219) --- .../calendula/ui/month/MonthChipTime.kt | 41 ++++++++++++++++++- .../calendula/ui/month/MonthScreen.kt | 9 ++-- .../calendula/ui/month/MonthChipTimeTest.kt | 22 ++++++++++ 3 files changed, 68 insertions(+), 4 deletions(-) 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 9ebf705..7163158 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 @@ -3,12 +3,20 @@ 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 kotlinx.datetime.TimeZone @@ -44,6 +52,36 @@ private const val TITLE_SAMPLE = "notepad" 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 @@ -78,7 +116,8 @@ internal fun rememberMonthTimeChipWidth(): Dp { val locale = currentLocale() val sample = formatTimeOfDay(SAMPLE_HOUR, SAMPLE_MINUTE, LocalUse24HourFormat.current, locale) return remember(sample, style, density, measurer) { - val textPx = measurer.measure("$sample $TITLE_SAMPLE", style).size.width + val label = monthChipLabel(sample, TITLE_SAMPLE, Color.Unspecified) + val textPx = measurer.measure(label, style).size.width with(density) { textPx.toDp() } + 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 8d37409..90bd503 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 @@ -147,6 +147,7 @@ import de.jeanlucmakiola.calendula.ui.common.declinedDecoration 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.eventAccent import de.jeanlucmakiola.calendula.ui.common.EventChipShape import de.jeanlucmakiola.calendula.ui.common.monthBarShape @@ -2471,11 +2472,13 @@ private fun MonthBar( null } val title = event.title.ifBlank { stringResource(R.string.event_untitled) } - val label = if (time != null) "$time $title" else title val dimCutoff = LocalDimCutoff.current val dimmed = dimCutoff != null && event.hasEnded(dimCutoff) val soften = LocalSoftenColors.current 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 moveAction = eventMoveAction(event) // The source stays put as a ghost while its floating copy travels. val monthDrag = LocalMonthDrag.current @@ -2489,7 +2492,7 @@ private fun MonthBar( .background(fill, shape) .padding(horizontal = MONTH_CHIP_TEXT_PADDING) .semantics { - contentDescription = label + contentDescription = label.text if (moveAction != null) customActions = listOf(moveAction) }, contentAlignment = Alignment.CenterStart, @@ -2501,7 +2504,7 @@ private fun MonthBar( maxLines = 1, overflow = titleOverflow.overflow, softWrap = titleOverflow.softWrap, - color = eventInk(fill), + color = eventInk(fill, alpha = TITLE_INK_ALPHA), textDecoration = declinedDecoration(event.isDeclined), ) } 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 c517be7..21698d6 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 @@ -1,5 +1,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.domain.EventInstance import kotlinx.datetime.DateTimeUnit @@ -55,6 +57,26 @@ class MonthChipTimeTest { assertThat(time).isEqualTo("2:30 pm") } + @Test + fun `the time is set apart from the title it precedes`() { + val label = monthChipLabel("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). + val spans = label.spanStyles + assertThat(spans).hasSize(1) + assertThat(spans.single().start).isEqualTo(0) + assertThat(spans.single().end).isEqualTo("09:05".length) + assertThat(spans.single().item.fontWeight).isEqualTo(FontWeight.Normal) + } + + @Test + fun `a chip without a time is styled title and nothing else`() { + val label = monthChipLabel(null, "Standup", Color.Black) + assertThat(label.text).isEqualTo("Standup") + assertThat(label.spanStyles).isEmpty() + } + @Test fun `an all-day chip never shows a time`() { val time = monthChipTime(allDay(), continuesLeft = false, zone, is24Hour = true, locale)