Set event times in regular weight across the timeline views (#219)
This commit is contained in:
@@ -172,7 +172,8 @@ fun BlockTimeLabel(
|
|||||||
) { text ->
|
) { text ->
|
||||||
Text(
|
Text(
|
||||||
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,
|
maxLines = maxLines,
|
||||||
overflow = overflow.overflow,
|
overflow = overflow.overflow,
|
||||||
softWrap = overflow.softWrap,
|
softWrap = overflow.softWrap,
|
||||||
|
|||||||
@@ -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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -738,7 +738,8 @@ private fun DragCopy(
|
|||||||
if (label != null) {
|
if (label != null) {
|
||||||
Text(
|
Text(
|
||||||
text = label,
|
text = label,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
// As the block it lifted off sets its own time (#219).
|
||||||
|
style = MaterialTheme.typography.labelSmall.asEventTime(),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = titleOverflow.overflow,
|
overflow = titleOverflow.overflow,
|
||||||
softWrap = titleOverflow.softWrap,
|
softWrap = titleOverflow.softWrap,
|
||||||
|
|||||||
@@ -2,23 +2,11 @@ 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.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.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.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.calendula.ui.common.rememberInlineTimeWidth
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.datetime.toLocalDateTime
|
import kotlinx.datetime.toLocalDateTime
|
||||||
import java.util.Locale
|
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. */
|
/** 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
|
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
|
* 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
|
||||||
@@ -99,25 +37,7 @@ internal fun monthChipTime(
|
|||||||
return formatTimeOfDay(start.hour, start.minute, is24Hour, locale)
|
return formatTimeOfDay(start.hour, start.minute, is24Hour, locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** The narrowest chip that may show a time — the text it needs, plus its chrome. */
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun rememberMonthTimeChipWidth(): Dp {
|
internal fun rememberMonthTimeChipWidth(): Dp =
|
||||||
val measurer = rememberTextMeasurer()
|
rememberInlineTimeWidth(MaterialTheme.typography.labelSmall) + CHIP_CHROME
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -148,6 +148,8 @@ 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.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.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
|
||||||
@@ -2478,7 +2480,7 @@ private fun MonthBar(
|
|||||||
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 same title/secondary ink pairing the week and day blocks use, with
|
||||||
// the time on the quieter half.
|
// 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)
|
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
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package de.jeanlucmakiola.calendula.ui.month
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.ui.common.inlineTimeLabel
|
||||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
import kotlinx.datetime.DateTimeUnit
|
import kotlinx.datetime.DateTimeUnit
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
@@ -59,7 +60,7 @@ class MonthChipTimeTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `the time is set apart from the title it precedes`() {
|
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")
|
assertThat(label.text).isEqualTo("09:05 Standup")
|
||||||
// Only the time is restyled: the title keeps labelSmall as the theme
|
// 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).
|
// sets it, so the two read as separate things on one line (#219).
|
||||||
@@ -72,7 +73,7 @@ class MonthChipTimeTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `a chip without a time is styled title and nothing else`() {
|
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.text).isEqualTo("Standup")
|
||||||
assertThat(label.spanStyles).isEmpty()
|
assertThat(label.spanStyles).isEmpty()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user