Set the month chip time apart from its title (#219)

This commit is contained in:
2026-09-04 11:56:36 +02:00
parent f7e9990c4e
commit 065afdb2b8
3 changed files with 68 additions and 4 deletions

View File

@@ -3,12 +3,20 @@ package de.jeanlucmakiola.calendula.ui.month
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity 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.rememberTextMeasurer
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
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.domain.EventInstance
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat 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.calendula.ui.common.formatTimeOfDay
import de.jeanlucmakiola.floret.locale.currentLocale import de.jeanlucmakiola.floret.locale.currentLocale
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
@@ -44,6 +52,36 @@ private const val TITLE_SAMPLE = "notepad"
private const val SAMPLE_HOUR = 12 private const val SAMPLE_HOUR = 12
private const val SAMPLE_MINUTE = 45 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 * 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 * 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 locale = currentLocale()
val sample = formatTimeOfDay(SAMPLE_HOUR, SAMPLE_MINUTE, LocalUse24HourFormat.current, locale) val sample = formatTimeOfDay(SAMPLE_HOUR, SAMPLE_MINUTE, LocalUse24HourFormat.current, locale)
return remember(sample, style, density, measurer) { 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 with(density) { textPx.toDp() } + CHIP_CHROME
} }
} }

View File

@@ -147,6 +147,7 @@ import de.jeanlucmakiola.calendula.ui.common.declinedDecoration
import de.jeanlucmakiola.calendula.ui.common.LocalDimCutoff import de.jeanlucmakiola.calendula.ui.common.LocalDimCutoff
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat 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.eventAccent
import de.jeanlucmakiola.calendula.ui.common.EventChipShape import de.jeanlucmakiola.calendula.ui.common.EventChipShape
import de.jeanlucmakiola.calendula.ui.common.monthBarShape import de.jeanlucmakiola.calendula.ui.common.monthBarShape
@@ -2471,11 +2472,13 @@ private fun MonthBar(
null null
} }
val title = event.title.ifBlank { stringResource(R.string.event_untitled) } val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
val label = if (time != null) "$time $title" else title
val dimCutoff = LocalDimCutoff.current val dimCutoff = LocalDimCutoff.current
val dimmed = dimCutoff != null && event.hasEnded(dimCutoff) val dimmed = dimCutoff != null && event.hasEnded(dimCutoff)
val soften = LocalSoftenColors.current val soften = LocalSoftenColors.current
val fill = eventFill(event.color, dark, soften) 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) val moveAction = eventMoveAction(event)
// The source stays put as a ghost while its floating copy travels. // The source stays put as a ghost while its floating copy travels.
val monthDrag = LocalMonthDrag.current val monthDrag = LocalMonthDrag.current
@@ -2489,7 +2492,7 @@ private fun MonthBar(
.background(fill, shape) .background(fill, shape)
.padding(horizontal = MONTH_CHIP_TEXT_PADDING) .padding(horizontal = MONTH_CHIP_TEXT_PADDING)
.semantics { .semantics {
contentDescription = label contentDescription = label.text
if (moveAction != null) customActions = listOf(moveAction) if (moveAction != null) customActions = listOf(moveAction)
}, },
contentAlignment = Alignment.CenterStart, contentAlignment = Alignment.CenterStart,
@@ -2501,7 +2504,7 @@ private fun MonthBar(
maxLines = 1, maxLines = 1,
overflow = titleOverflow.overflow, overflow = titleOverflow.overflow,
softWrap = titleOverflow.softWrap, softWrap = titleOverflow.softWrap,
color = eventInk(fill), color = eventInk(fill, alpha = TITLE_INK_ALPHA),
textDecoration = declinedDecoration(event.isDeclined), textDecoration = declinedDecoration(event.isDeclined),
) )
} }

View File

@@ -1,5 +1,7 @@
package de.jeanlucmakiola.calendula.ui.month 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 com.google.common.truth.Truth.assertThat
import de.jeanlucmakiola.calendula.domain.EventInstance import de.jeanlucmakiola.calendula.domain.EventInstance
import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.DateTimeUnit
@@ -55,6 +57,26 @@ class MonthChipTimeTest {
assertThat(time).isEqualTo("2:30 pm") 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 @Test
fun `an all-day chip never shows a time`() { fun `an all-day chip never shows a time`() {
val time = monthChipTime(allDay(), continuesLeft = false, zone, is24Hour = true, locale) val time = monthChipTime(allDay(), continuesLeft = false, zone, is24Hour = true, locale)