Hold the time apart from the title on any font (#219)
The medium-to-regular step is a no-op on Atkinson Hyperlegible, JetBrains Mono and imported fonts, which ship no medium face — so the title takes full ink against the time's 0.8 instead, and the four title sites share the constant. Also: the drag copy carries the time its source chip had rather than deriving one with continuesLeft hardcoded false; the month grid takes its zone from the state; times are formatted once per row and the chip width measured once per grid; the chip geometry lives in one file; and a screen reader hears the time whether or not the chip is wide enough to draw it.
This commit is contained in:
@@ -13,6 +13,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
the title now simply runs to the edge of its chip — a few more letters per
|
the title now simply runs to the edge of its chip — a few more letters per
|
||||||
cell, which is often the difference between two events you can tell apart and
|
cell, which is often the difference between two events you can tell apart and
|
||||||
two you can't ([#164]).
|
two you can't ([#164]).
|
||||||
|
- Month-view events now show their start time before the title, where the chip
|
||||||
|
is wide enough to keep the title readable — in landscape, on a foldable and on
|
||||||
|
a tablet, and never at the cost of a narrow phone column. All-day events show
|
||||||
|
no time, and neither does a bar carried in from the previous week, whose start
|
||||||
|
is not in that row. Event times across the month, week and day views are also
|
||||||
|
set in a regular weight against the title's, so a time reads as a time rather
|
||||||
|
than as part of the name next to it ([#219]).
|
||||||
- The calendar titles now shorten instead of being cut off. When the full month
|
- The calendar titles now shorten instead of being cut off. When the full month
|
||||||
name doesn't fit the top bar, the month and week views fall back to its
|
name doesn't fit the top bar, the month and week views fall back to its
|
||||||
three-letter form and the day view drops the weekday, rather than trailing off
|
three-letter form and the day view drops the weekday, rather than trailing off
|
||||||
@@ -1583,5 +1590,6 @@ automatically, with zero telemetry and no internet permission.
|
|||||||
[#225]: https://codeberg.org/jlmakiola/calendula/issues/225
|
[#225]: https://codeberg.org/jlmakiola/calendula/issues/225
|
||||||
[#228]: https://codeberg.org/jlmakiola/calendula/issues/228
|
[#228]: https://codeberg.org/jlmakiola/calendula/issues/228
|
||||||
[#234]: https://codeberg.org/jlmakiola/calendula/issues/234
|
[#234]: https://codeberg.org/jlmakiola/calendula/issues/234
|
||||||
|
[#219]: https://codeberg.org/jlmakiola/calendula/issues/219
|
||||||
[#248]: https://codeberg.org/jlmakiola/calendula/issues/248
|
[#248]: https://codeberg.org/jlmakiola/calendula/issues/248
|
||||||
[#253]: https://codeberg.org/jlmakiola/calendula/issues/253
|
[#253]: https://codeberg.org/jlmakiola/calendula/issues/253
|
||||||
|
|||||||
@@ -15,32 +15,26 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import de.jeanlucmakiola.floret.locale.currentLocale
|
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_WEIGHT = FontWeight.Normal
|
||||||
private val TIME_TRACKING = 0.sp
|
private val TIME_TRACKING = 0.sp
|
||||||
|
private val TIME_SPAN = SpanStyle(fontWeight = TIME_WEIGHT, letterSpacing = TIME_TRACKING)
|
||||||
|
|
||||||
/** Ink for a title, against [SECONDARY_INK_ALPHA] for the time beside it. */
|
/**
|
||||||
const val TITLE_INK_ALPHA = 0.85f
|
* Ink for an event's title, against [SECONDARY_INK_ALPHA] for the time beside or
|
||||||
|
* beneath it. Full, so the pair separates on a typeface with no medium weight to
|
||||||
|
* step down from — Atkinson Hyperlegible and JetBrains Mono ship regular and bold
|
||||||
|
* only, and a user's imported font a single face, so [asEventTime] is a no-op
|
||||||
|
* there and the ink is the whole difference (#219).
|
||||||
|
*/
|
||||||
|
const val TITLE_INK_ALPHA = 1f
|
||||||
|
|
||||||
/** [this] set as an event's time rather than as its title. */
|
/**
|
||||||
|
* [this] set as an event's time rather than as its title: the label styles' medium
|
||||||
|
* weight and 0.5sp of tracking left a time reading as part of the title next to it.
|
||||||
|
*/
|
||||||
fun TextStyle.asEventTime(): TextStyle =
|
fun TextStyle.asEventTime(): TextStyle =
|
||||||
copy(fontWeight = TIME_WEIGHT, letterSpacing = TIME_TRACKING)
|
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]. */
|
/** Text reading [time], set apart in [timeInk], before [title]. */
|
||||||
fun inlineTimeLabel(time: String?, title: String, timeInk: Color): AnnotatedString =
|
fun inlineTimeLabel(time: String?, title: String, timeInk: Color): AnnotatedString =
|
||||||
buildAnnotatedString {
|
buildAnnotatedString {
|
||||||
@@ -53,17 +47,8 @@ fun inlineTimeLabel(time: String?, title: String, timeInk: Color): AnnotatedStri
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* The characters of title that have to survive a time prefix for it to be worth
|
* 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
|
* its place — lowercase Latin of average advance, priced at the surface's own
|
||||||
* characters and priced at the surface's own text style rather than guessed in
|
* text style rather than guessed in dp.
|
||||||
* 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"
|
private const val TITLE_SAMPLE = "notepad"
|
||||||
|
|
||||||
@@ -77,8 +62,7 @@ private const val SAMPLE_MINUTE = 45
|
|||||||
*
|
*
|
||||||
* Measured rather than a device breakpoint, so it follows the font scale, the
|
* 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
|
* 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
|
* already been taken off the width.
|
||||||
* tablet all come out wide enough without any of them being named.
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberInlineTimeWidth(style: TextStyle): Dp {
|
fun rememberInlineTimeWidth(style: TextStyle): Dp {
|
||||||
|
|||||||
@@ -733,7 +733,7 @@ private fun DragCopy(
|
|||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = titleOverflow.overflow,
|
overflow = titleOverflow.overflow,
|
||||||
softWrap = titleOverflow.softWrap,
|
softWrap = titleOverflow.softWrap,
|
||||||
color = eventInk(fill, alpha = 0.85f),
|
color = eventInk(fill, alpha = TITLE_INK_ALPHA),
|
||||||
)
|
)
|
||||||
if (label != null) {
|
if (label != null) {
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -119,7 +119,9 @@ import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.LocalShowHourLines
|
import de.jeanlucmakiola.calendula.ui.common.LocalShowHourLines
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalTimelineZoom
|
import de.jeanlucmakiola.calendula.ui.common.LocalTimelineZoom
|
||||||
import de.jeanlucmakiola.calendula.ui.common.MIN_EVENT_FRACTION
|
import de.jeanlucmakiola.calendula.ui.common.MIN_EVENT_FRACTION
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.asEventTime
|
||||||
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.hourHeight
|
import de.jeanlucmakiola.calendula.ui.common.hourHeight
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberTimelinePinchZoom
|
import de.jeanlucmakiola.calendula.ui.common.rememberTimelinePinchZoom
|
||||||
import de.jeanlucmakiola.calendula.ui.common.formatMinuteOfDay
|
import de.jeanlucmakiola.calendula.ui.common.formatMinuteOfDay
|
||||||
@@ -526,7 +528,7 @@ private fun AllDayBar(
|
|||||||
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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -749,7 +751,9 @@ private fun EventBlock(
|
|||||||
val timeMaxLines = if (showTime && spare >= timeLineHeight) {
|
val timeMaxLines = if (showTime && spare >= timeLineHeight) {
|
||||||
blockTextLines(
|
blockTextLines(
|
||||||
text = timeLabel,
|
text = timeLabel,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
// The style it is drawn in, or the budget measures a line the label
|
||||||
|
// never uses (#219).
|
||||||
|
style = MaterialTheme.typography.labelSmall.asEventTime(),
|
||||||
textWidth = textWidth,
|
textWidth = textWidth,
|
||||||
max = MAX_TIME_LINES,
|
max = MAX_TIME_LINES,
|
||||||
)
|
)
|
||||||
@@ -800,7 +804,7 @@ private fun EventBlock(
|
|||||||
title = title,
|
title = title,
|
||||||
maxLines = titleMaxLines,
|
maxLines = titleMaxLines,
|
||||||
textWidth = textWidth,
|
textWidth = textWidth,
|
||||||
color = eventInk(fill, alpha = 0.85f),
|
color = eventInk(fill, alpha = TITLE_INK_ALPHA),
|
||||||
textDecoration = declinedDecoration(block.event.isDeclined),
|
textDecoration = declinedDecoration(block.event.isDeclined),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.ui.month
|
||||||
|
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
// The month grid's chip geometry, together in one file: these derive from each
|
||||||
|
// other, and a top-level val reading one across a file boundary would resolve
|
||||||
|
// against whichever class the JVM happened to initialise first.
|
||||||
|
|
||||||
|
/** Gap between a day cell and its neighbours. */
|
||||||
|
internal val CELL_GAP = 2.dp
|
||||||
|
|
||||||
|
/** Padding between a month chip's edge and its text. */
|
||||||
|
internal val MONTH_CHIP_TEXT_PADDING = 4.dp
|
||||||
|
|
||||||
|
/** A chip's own inset inside its day cell, on top of the cell's gap. */
|
||||||
|
internal val MONTH_CHIP_INSET = CELL_GAP + 1.dp
|
||||||
|
|
||||||
|
/** Horizontal space a chip spends on chrome rather than on text, both sides. */
|
||||||
|
internal val MONTH_CHIP_CHROME = (MONTH_CHIP_INSET + MONTH_CHIP_TEXT_PADDING) * 2f
|
||||||
@@ -3,7 +3,6 @@ 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.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
import de.jeanlucmakiola.calendula.ui.common.formatTimeOfDay
|
import de.jeanlucmakiola.calendula.ui.common.formatTimeOfDay
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberInlineTimeWidth
|
import de.jeanlucmakiola.calendula.ui.common.rememberInlineTimeWidth
|
||||||
@@ -11,15 +10,6 @@ import kotlinx.datetime.TimeZone
|
|||||||
import kotlinx.datetime.toLocalDateTime
|
import kotlinx.datetime.toLocalDateTime
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
/** Padding between a month chip's edge and its text. */
|
|
||||||
internal val MONTH_CHIP_TEXT_PADDING = 4.dp
|
|
||||||
|
|
||||||
/** A chip's own inset inside its day cell, on top of the cell's gap. */
|
|
||||||
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 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
|
||||||
@@ -40,4 +30,4 @@ internal fun monthChipTime(
|
|||||||
/** The narrowest chip that may show a time — the text it needs, plus its chrome. */
|
/** The narrowest chip that may show a time — the text it needs, plus its chrome. */
|
||||||
@Composable
|
@Composable
|
||||||
internal fun rememberMonthTimeChipWidth(): Dp =
|
internal fun rememberMonthTimeChipWidth(): Dp =
|
||||||
rememberInlineTimeWidth(MaterialTheme.typography.labelSmall) + CHIP_CHROME
|
rememberInlineTimeWidth(MaterialTheme.typography.labelSmall) + MONTH_CHIP_CHROME
|
||||||
|
|||||||
@@ -97,6 +97,13 @@ data class MonthChipDrag(
|
|||||||
val targetDate: LocalDate?,
|
val targetDate: LocalDate?,
|
||||||
val topLeftInRoot: Offset,
|
val topLeftInRoot: Offset,
|
||||||
val sizePx: IntSize,
|
val sizePx: IntSize,
|
||||||
|
/**
|
||||||
|
* The start time of the chip this lifted off, already formatted (#219).
|
||||||
|
* Carried rather than re-derived: the overlay has no row to ask whether the
|
||||||
|
* bar was carried in from the previous week, and no zone the grid laid out
|
||||||
|
* in. Null where the source chip had no time to show.
|
||||||
|
*/
|
||||||
|
val time: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Where a finished chip drag asks its event to go, as a whole-day shift. */
|
/** Where a finished chip drag asks its event to go, as a whole-day shift. */
|
||||||
@@ -172,6 +179,7 @@ class MonthDragController {
|
|||||||
|
|
||||||
private var event: EventInstance? = null
|
private var event: EventInstance? = null
|
||||||
private var grabDate: LocalDate? = null
|
private var grabDate: LocalDate? = null
|
||||||
|
private var time: String? = null
|
||||||
private var grab = Offset.Zero
|
private var grab = Offset.Zero
|
||||||
private var pointer = Offset.Zero
|
private var pointer = Offset.Zero
|
||||||
private var sizePx = IntSize.Zero
|
private var sizePx = IntSize.Zero
|
||||||
@@ -190,9 +198,11 @@ class MonthDragController {
|
|||||||
pointerInRoot: Offset,
|
pointerInRoot: Offset,
|
||||||
chipInRoot: Offset,
|
chipInRoot: Offset,
|
||||||
size: IntSize,
|
size: IntSize,
|
||||||
|
time: String? = null,
|
||||||
) {
|
) {
|
||||||
this.event = event
|
this.event = event
|
||||||
this.grabDate = grabDate
|
this.grabDate = grabDate
|
||||||
|
this.time = time
|
||||||
settling = null
|
settling = null
|
||||||
isDragging = true
|
isDragging = true
|
||||||
liftedInstanceId = event.instanceId
|
liftedInstanceId = event.instanceId
|
||||||
@@ -210,6 +220,7 @@ class MonthDragController {
|
|||||||
fun cancel() {
|
fun cancel() {
|
||||||
event = null
|
event = null
|
||||||
grabDate = null
|
grabDate = null
|
||||||
|
time = null
|
||||||
isDragging = false
|
isDragging = false
|
||||||
liftedInstanceId = null
|
liftedInstanceId = null
|
||||||
drag = null
|
drag = null
|
||||||
@@ -331,6 +342,7 @@ class MonthDragController {
|
|||||||
targetDate = resolved ?: drag?.targetDate,
|
targetDate = resolved ?: drag?.targetDate,
|
||||||
topLeftInRoot = pointer - grab,
|
topLeftInRoot = pointer - grab,
|
||||||
sizePx = sizePx,
|
sizePx = sizePx,
|
||||||
|
time = time,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ import androidx.compose.ui.semantics.semantics
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
@@ -561,8 +562,10 @@ private fun MonthDragOverlay(controller: MonthDragController) {
|
|||||||
dark = dark,
|
dark = dark,
|
||||||
continuesLeft = false,
|
continuesLeft = false,
|
||||||
continuesRight = false,
|
continuesRight = false,
|
||||||
// The copy is a cut-out of the chip it lifted off, so it answers the
|
// The time the source chip carried, drawn only if this one-column
|
||||||
// gate at the same width the grid did.
|
// cut-out of it has the room — which a slice of a multi-day bar has
|
||||||
|
// not, however wide the bar was.
|
||||||
|
time = drag.time,
|
||||||
showTime = with(density) { drag.sizePx.width.toDp() } >= timeChipWidth,
|
showTime = with(density) { drag.sizePx.width.toDp() } >= timeChipWidth,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
// Absolute: these are root coordinates, and the direction-aware
|
// Absolute: these are root coordinates, and the direction-aware
|
||||||
@@ -766,7 +769,6 @@ private val DAY_NUMBER_HEIGHT = 22.dp
|
|||||||
private val WEEK_NUMBER_GUTTER = 40.dp
|
private val WEEK_NUMBER_GUTTER = 40.dp
|
||||||
private val DAY_NUMBER_GAP = 4.dp
|
private val DAY_NUMBER_GAP = 4.dp
|
||||||
private val CELL_TOP_PADDING = 6.dp
|
private val CELL_TOP_PADDING = 6.dp
|
||||||
internal val CELL_GAP = 2.dp
|
|
||||||
/** Named separately because the split style's selection outline draws its own
|
/** Named separately because the split style's selection outline draws its own
|
||||||
* rounded rect and has to match this radius exactly. */
|
* rounded rect and has to match this radius exactly. */
|
||||||
private val CELL_CORNER = 12.dp
|
private val CELL_CORNER = 12.dp
|
||||||
@@ -815,10 +817,15 @@ internal fun MonthGrid(
|
|||||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
) {
|
) {
|
||||||
val month = state.month
|
val month = state.month
|
||||||
|
// Once per grid: the value depends on the typography, the density, the
|
||||||
|
// locale and the 24-hour setting, none of which vary by row (#219).
|
||||||
|
val timeChipWidth = rememberMonthTimeChipWidth()
|
||||||
state.weeks.forEach { week ->
|
state.weeks.forEach { week ->
|
||||||
MonthWeekRow(
|
MonthWeekRow(
|
||||||
week = week,
|
week = week,
|
||||||
today = state.today,
|
today = state.today,
|
||||||
|
zone = state.zone,
|
||||||
|
timeChipWidth = timeChipWidth,
|
||||||
inMonth = { it.month == month.month && it.year == month.year },
|
inMonth = { it.month == month.month && it.year == month.year },
|
||||||
showWeekNumbers = showWeekNumbers,
|
showWeekNumbers = showWeekNumbers,
|
||||||
onOpenDay = onOpenDay,
|
onOpenDay = onOpenDay,
|
||||||
@@ -858,6 +865,7 @@ internal fun ContinuousMonthGrid(
|
|||||||
) {
|
) {
|
||||||
val monthCount = remember { continuousMonthCount() }
|
val monthCount = remember { continuousMonthCount() }
|
||||||
val todayMonth = remember(state.today) { YearMonth(state.today.year, state.today.month) }
|
val todayMonth = remember(state.today) { YearMonth(state.today.year, state.today.month) }
|
||||||
|
val timeChipWidth = rememberMonthTimeChipWidth()
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
@@ -879,6 +887,8 @@ internal fun ContinuousMonthGrid(
|
|||||||
weeks = state.monthsByIndex[index],
|
weeks = state.monthsByIndex[index],
|
||||||
weekStart = state.weekStart,
|
weekStart = state.weekStart,
|
||||||
today = state.today,
|
today = state.today,
|
||||||
|
zone = state.zone,
|
||||||
|
timeChipWidth = timeChipWidth,
|
||||||
showWeekNumbers = showWeekNumbers,
|
showWeekNumbers = showWeekNumbers,
|
||||||
onOpenDay = onOpenDay,
|
onOpenDay = onOpenDay,
|
||||||
onEventClick = onEventClick,
|
onEventClick = onEventClick,
|
||||||
@@ -899,6 +909,8 @@ private fun ContinuousMonthBlock(
|
|||||||
weeks: List<MonthWeek>?,
|
weeks: List<MonthWeek>?,
|
||||||
weekStart: DayOfWeek,
|
weekStart: DayOfWeek,
|
||||||
today: LocalDate,
|
today: LocalDate,
|
||||||
|
zone: TimeZone,
|
||||||
|
timeChipWidth: Dp,
|
||||||
showWeekNumbers: Boolean,
|
showWeekNumbers: Boolean,
|
||||||
onOpenDay: (LocalDate) -> Unit,
|
onOpenDay: (LocalDate) -> Unit,
|
||||||
onEventClick: (EventInstance) -> Unit,
|
onEventClick: (EventInstance) -> Unit,
|
||||||
@@ -918,6 +930,8 @@ private fun ContinuousMonthBlock(
|
|||||||
MonthWeekRow(
|
MonthWeekRow(
|
||||||
week = week,
|
week = week,
|
||||||
today = today,
|
today = today,
|
||||||
|
zone = zone,
|
||||||
|
timeChipWidth = timeChipWidth,
|
||||||
inMonth = { it.month == month.month && it.year == month.year },
|
inMonth = { it.month == month.month && it.year == month.year },
|
||||||
// The block owns its month alone: a day from either
|
// The block owns its month alone: a day from either
|
||||||
// neighbour is left out entirely rather than dimmed.
|
// neighbour is left out entirely rather than dimmed.
|
||||||
@@ -987,6 +1001,7 @@ internal fun DenseMonthGrid(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val weekCount = remember(state.weekStart) { continuousWeekCount(state.weekStart) }
|
val weekCount = remember(state.weekStart) { continuousWeekCount(state.weekStart) }
|
||||||
|
val timeChipWidth = rememberMonthTimeChipWidth()
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -1009,6 +1024,8 @@ internal fun DenseMonthGrid(
|
|||||||
MonthWeekRow(
|
MonthWeekRow(
|
||||||
week = week,
|
week = week,
|
||||||
today = state.today,
|
today = state.today,
|
||||||
|
zone = state.zone,
|
||||||
|
timeChipWidth = timeChipWidth,
|
||||||
// Every day in the stream belongs to a month equally — there
|
// Every day in the stream belongs to a month equally — there
|
||||||
// is no "other month" to recede here.
|
// is no "other month" to recede here.
|
||||||
inMonth = { true },
|
inMonth = { true },
|
||||||
@@ -1894,6 +1911,10 @@ private fun rememberSkeletonPulse(): Float {
|
|||||||
private fun MonthWeekRow(
|
private fun MonthWeekRow(
|
||||||
week: MonthWeek,
|
week: MonthWeek,
|
||||||
today: LocalDate,
|
today: LocalDate,
|
||||||
|
/** The zone the grid was laid out in — never re-read here (see [MonthUiState.Success.zone]). */
|
||||||
|
zone: TimeZone,
|
||||||
|
/** The narrowest chip that may carry a start time, measured once per grid (#219). */
|
||||||
|
timeChipWidth: Dp,
|
||||||
inMonth: (LocalDate) -> Boolean,
|
inMonth: (LocalDate) -> Boolean,
|
||||||
showWeekNumbers: Boolean,
|
showWeekNumbers: Boolean,
|
||||||
onOpenDay: (LocalDate) -> Unit,
|
onOpenDay: (LocalDate) -> Unit,
|
||||||
@@ -1913,6 +1934,27 @@ private fun MonthWeekRow(
|
|||||||
val laneCount = (week.spans.maxOfOrNull { it.lane } ?: -1) + 1
|
val laneCount = (week.spans.maxOfOrNull { it.lane } ?: -1) + 1
|
||||||
val shownLanes = laneCount.coerceAtMost(MAX_EVENT_ROWS)
|
val shownLanes = laneCount.coerceAtMost(MAX_EVENT_ROWS)
|
||||||
val morphing = morphInFlight()
|
val morphing = morphInFlight()
|
||||||
|
// Every chip's start time for this row at once, and only when the row's own
|
||||||
|
// inputs change: formatting is a parsed pattern per call, and the dim cutoff
|
||||||
|
// ticks every minute while a drag recomposes every frame (#219).
|
||||||
|
val is24Hour = LocalUse24HourFormat.current
|
||||||
|
val locale = currentLocale()
|
||||||
|
val chipTimes = remember(week, zone, is24Hour, locale) {
|
||||||
|
buildMap {
|
||||||
|
week.spans.forEach { span ->
|
||||||
|
put(
|
||||||
|
span.event.instanceId,
|
||||||
|
monthChipTime(span.event, span.continuesLeft, zone, is24Hour, locale),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
week.timedByDay.values.flatten().forEach { event ->
|
||||||
|
put(
|
||||||
|
event.instanceId,
|
||||||
|
monthChipTime(event, continuesLeft = false, zone, is24Hour, locale),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Drag to reschedule (#68). The chips can take no pointer input of their own
|
// Drag to reschedule (#68). The chips can take no pointer input of their own
|
||||||
// — the full-bleed tap layer sits on top of them — so one detector on the
|
// — the full-bleed tap layer sits on top of them — so one detector on the
|
||||||
@@ -1987,14 +2029,13 @@ private fun MonthWeekRow(
|
|||||||
band = bandCoordinates,
|
band = bandCoordinates,
|
||||||
rowHeightPx = rowHeightPx,
|
rowHeightPx = rowHeightPx,
|
||||||
isRtl = isRtl,
|
isRtl = isRtl,
|
||||||
|
chipTimes = chipTimes,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
// What a chip has to spend, against the [timeChipWidth] a start time
|
||||||
|
// costs it (#219).
|
||||||
val colW = maxWidth / 7
|
val colW = maxWidth / 7
|
||||||
// The width a chip needs before its start time is worth the title
|
|
||||||
// characters it costs (#219) — measured here, so the same column in
|
|
||||||
// landscape or on a foldable answers differently on its own.
|
|
||||||
val timeChipWidth = rememberMonthTimeChipWidth()
|
|
||||||
|
|
||||||
// Per-day background pills — same surfaceContainer rounded surface the
|
// Per-day background pills — same surfaceContainer rounded surface the
|
||||||
// week/day views use, so the three views share one visual language.
|
// week/day views use, so the three views share one visual language.
|
||||||
@@ -2074,6 +2115,7 @@ private fun MonthWeekRow(
|
|||||||
continuesLeft = span.continuesLeft,
|
continuesLeft = span.continuesLeft,
|
||||||
continuesRight = span.continuesRight,
|
continuesRight = span.continuesRight,
|
||||||
days = week.days.subList(span.startCol, span.endCol + 1),
|
days = week.days.subList(span.startCol, span.endCol + 1),
|
||||||
|
time = chipTimes[span.event.instanceId],
|
||||||
showTime = colW * cols >= timeChipWidth,
|
showTime = colW * cols >= timeChipWidth,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.offset(
|
.offset(
|
||||||
@@ -2145,6 +2187,7 @@ private fun MonthWeekRow(
|
|||||||
continuesLeft = false,
|
continuesLeft = false,
|
||||||
continuesRight = false,
|
continuesRight = false,
|
||||||
days = listOf(d),
|
days = listOf(d),
|
||||||
|
time = chipTimes[ev.instanceId],
|
||||||
showTime = colW >= timeChipWidth,
|
showTime = colW >= timeChipWidth,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.offset(
|
.offset(
|
||||||
@@ -2306,6 +2349,9 @@ private fun monthChipDragModifier(
|
|||||||
band: Array<LayoutCoordinates?>,
|
band: Array<LayoutCoordinates?>,
|
||||||
rowHeightPx: Float,
|
rowHeightPx: Float,
|
||||||
isRtl: Boolean,
|
isRtl: Boolean,
|
||||||
|
/** The row's formatted chip times by instance id, so the copy carries the
|
||||||
|
* one its source chip had rather than deriving another (#219). */
|
||||||
|
chipTimes: Map<Long, String?>,
|
||||||
): Modifier = rememberDragSurface(
|
): Modifier = rememberDragSurface(
|
||||||
enabled = moveScope?.dragEnabled == true && controller != null,
|
enabled = moveScope?.dragEnabled == true && controller != null,
|
||||||
key = week.days.first(),
|
key = week.days.first(),
|
||||||
@@ -2334,6 +2380,7 @@ private fun monthChipDragModifier(
|
|||||||
y = requireNotNull(bandTop) + lane * rowHeightPx,
|
y = requireNotNull(bandTop) + lane * rowHeightPx,
|
||||||
),
|
),
|
||||||
size = IntSize(columnPx.toInt(), rowHeightPx.toInt()),
|
size = IntSize(columnPx.toInt(), rowHeightPx.toInt()),
|
||||||
|
time = chipTimes[event.instanceId],
|
||||||
)
|
)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -2458,21 +2505,11 @@ private fun MonthBar(
|
|||||||
* the provider hands the re-read instance a new one.
|
* the provider hands the re-read instance a new one.
|
||||||
*/
|
*/
|
||||||
days: List<LocalDate>? = null,
|
days: List<LocalDate>? = null,
|
||||||
/** Whether this chip has the width to carry its start time (#219). */
|
/** The chip's start time, or null where it has none — see `monthChipTime` (#219). */
|
||||||
|
time: String? = null,
|
||||||
|
/** Whether this chip has the width to draw [time]; a screen reader gets it either way. */
|
||||||
showTime: Boolean = false,
|
showTime: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val zone = remember { TimeZone.currentSystemDefault() }
|
|
||||||
val time = if (showTime) {
|
|
||||||
monthChipTime(
|
|
||||||
event = event,
|
|
||||||
continuesLeft = continuesLeft,
|
|
||||||
zone = zone,
|
|
||||||
is24Hour = LocalUse24HourFormat.current,
|
|
||||||
locale = currentLocale(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
|
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
|
||||||
val dimCutoff = LocalDimCutoff.current
|
val dimCutoff = LocalDimCutoff.current
|
||||||
val dimmed = dimCutoff != null && event.hasEnded(dimCutoff)
|
val dimmed = dimCutoff != null && event.hasEnded(dimCutoff)
|
||||||
@@ -2480,7 +2517,15 @@ 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 = inlineTimeLabel(time, title, eventInk(fill, alpha = SECONDARY_INK_ALPHA))
|
val label = inlineTimeLabel(
|
||||||
|
time = time.takeIf { showTime },
|
||||||
|
title = title,
|
||||||
|
timeInk = eventInk(fill, alpha = SECONDARY_INK_ALPHA),
|
||||||
|
)
|
||||||
|
// Announced whether or not it is drawn, and comma-separated as the week and
|
||||||
|
// day blocks do it: what a screen reader hears shouldn't turn on how wide
|
||||||
|
// the chip happens to be (#219).
|
||||||
|
val description = if (time != null) "$title, $time" else title
|
||||||
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
|
||||||
@@ -2494,7 +2539,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.text
|
contentDescription = description
|
||||||
if (moveAction != null) customActions = listOf(moveAction)
|
if (moveAction != null) customActions = listOf(moveAction)
|
||||||
},
|
},
|
||||||
contentAlignment = Alignment.CenterStart,
|
contentAlignment = Alignment.CenterStart,
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ fun MonthWeek.laneEvents(col: Int, day: LocalDate, laneCap: Int): List<EventInst
|
|||||||
*/
|
*/
|
||||||
fun MonthWeek.chipAt(col: Int, lane: Int, laneCap: Int): EventInstance? {
|
fun MonthWeek.chipAt(col: Int, lane: Int, laneCap: Int): EventInstance? {
|
||||||
if (col !in days.indices || lane !in 0 until laneCap) return null
|
if (col !in days.indices || lane !in 0 until laneCap) return null
|
||||||
spans.firstOrNull { it.lane == lane && col in it.startCol..it.endCol }?.let { return it.event }
|
spanAt(col, lane)?.let { return it.event }
|
||||||
val occupied = spans
|
val occupied = spans
|
||||||
.filter { it.lane < laneCap && col in it.startCol..it.endCol }
|
.filter { it.lane < laneCap && col in it.startCol..it.endCol }
|
||||||
.map { it.lane }
|
.map { it.lane }
|
||||||
@@ -91,8 +91,11 @@ fun MonthWeek.chipAt(col: Int, lane: Int, laneCap: Int): EventInstance? {
|
|||||||
* [col] for a single-day chip, and for an empty slot no one should be asking
|
* [col] for a single-day chip, and for an empty slot no one should be asking
|
||||||
* about.
|
* about.
|
||||||
*/
|
*/
|
||||||
fun MonthWeek.chipStartCol(col: Int, lane: Int): Int =
|
fun MonthWeek.chipStartCol(col: Int, lane: Int): Int = spanAt(col, lane)?.startCol ?: col
|
||||||
spans.firstOrNull { it.lane == lane && col in it.startCol..it.endCol }?.startCol ?: col
|
|
||||||
|
/** The bar covering lane [lane] of column [col], or null where a pill sits there. */
|
||||||
|
fun MonthWeek.spanAt(col: Int, lane: Int): MonthSpan? =
|
||||||
|
spans.firstOrNull { it.lane == lane && col in it.startCol..it.endCol }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The events on [day] that [laneEvents] had no lane left for — its exact
|
* The events on [day] that [laneEvents] had no lane left for — its exact
|
||||||
@@ -136,6 +139,8 @@ sealed interface ContinuousMonthUiState {
|
|||||||
val monthsByIndex: Map<Int, List<MonthWeek>>,
|
val monthsByIndex: Map<Int, List<MonthWeek>>,
|
||||||
val weeksByIndex: Map<Int, MonthWeek>,
|
val weeksByIndex: Map<Int, MonthWeek>,
|
||||||
val weekStart: DayOfWeek,
|
val weekStart: DayOfWeek,
|
||||||
|
/** As [MonthUiState.Success.zone], and for the same reason. */
|
||||||
|
val zone: TimeZone = TimeZone.currentSystemDefault(),
|
||||||
) : ContinuousMonthUiState
|
) : ContinuousMonthUiState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -238,6 +238,7 @@ class MonthViewModel @Inject constructor(
|
|||||||
monthsByIndex = months,
|
monthsByIndex = months,
|
||||||
weeksByIndex = weeks,
|
weeksByIndex = weeks,
|
||||||
weekStart = weekStart,
|
weekStart = weekStart,
|
||||||
|
zone = zone,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,9 @@ import de.jeanlucmakiola.calendula.ui.common.LocalShowHourLines
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.LocalTimelineZoom
|
import de.jeanlucmakiola.calendula.ui.common.LocalTimelineZoom
|
||||||
import de.jeanlucmakiola.calendula.ui.common.MIN_EVENT_FRACTION
|
import de.jeanlucmakiola.calendula.ui.common.MIN_EVENT_FRACTION
|
||||||
import de.jeanlucmakiola.calendula.ui.common.MIN_TITLE_WRAP_WIDTH
|
import de.jeanlucmakiola.calendula.ui.common.MIN_TITLE_WRAP_WIDTH
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.asEventTime
|
||||||
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.hourHeight
|
import de.jeanlucmakiola.calendula.ui.common.hourHeight
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberTimelinePinchZoom
|
import de.jeanlucmakiola.calendula.ui.common.rememberTimelinePinchZoom
|
||||||
import de.jeanlucmakiola.calendula.ui.common.formatMinuteOfDay
|
import de.jeanlucmakiola.calendula.ui.common.formatMinuteOfDay
|
||||||
@@ -661,7 +663,7 @@ private fun AllDayBar(
|
|||||||
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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -916,7 +918,9 @@ private fun EventBlock(
|
|||||||
val timeMaxLines = if (showTime && spare >= timeLineHeight) {
|
val timeMaxLines = if (showTime && spare >= timeLineHeight) {
|
||||||
blockTextLines(
|
blockTextLines(
|
||||||
text = timeLabel,
|
text = timeLabel,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
// The style it is drawn in, or the budget measures a line the label
|
||||||
|
// never uses (#219).
|
||||||
|
style = MaterialTheme.typography.labelSmall.asEventTime(),
|
||||||
textWidth = textWidth,
|
textWidth = textWidth,
|
||||||
max = MAX_TIME_LINES,
|
max = MAX_TIME_LINES,
|
||||||
)
|
)
|
||||||
@@ -969,7 +973,7 @@ private fun EventBlock(
|
|||||||
title = title,
|
title = title,
|
||||||
maxLines = titleMaxLines,
|
maxLines = titleMaxLines,
|
||||||
textWidth = textWidth,
|
textWidth = textWidth,
|
||||||
color = eventInk(fill, alpha = 0.85f),
|
color = eventInk(fill, alpha = TITLE_INK_ALPHA),
|
||||||
textDecoration = declinedDecoration(block.event.isDeclined),
|
textDecoration = declinedDecoration(block.event.isDeclined),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.ui.common
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
/** How an event's time is set against its title (#219). */
|
||||||
|
class EventTimeStyleTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a time steps down to regular and closes its tracking up`() {
|
||||||
|
val label = TextStyle(fontWeight = FontWeight.Medium, letterSpacing = 0.5.sp)
|
||||||
|
val time = label.asEventTime()
|
||||||
|
assertThat(time.fontWeight).isEqualTo(FontWeight.Normal)
|
||||||
|
assertThat(time.letterSpacing).isEqualTo(0.sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `nothing else of the style is touched`() {
|
||||||
|
val label = TextStyle(
|
||||||
|
fontSize = 11.sp,
|
||||||
|
color = Color.Red,
|
||||||
|
textDecoration = TextDecoration.LineThrough,
|
||||||
|
)
|
||||||
|
val time = label.asEventTime()
|
||||||
|
assertThat(time.fontSize).isEqualTo(11.sp)
|
||||||
|
assertThat(time.color).isEqualTo(Color.Red)
|
||||||
|
assertThat(time.textDecoration).isEqualTo(TextDecoration.LineThrough)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `the title is inked above the time beside it`() {
|
||||||
|
assertThat(TITLE_INK_ALPHA).isGreaterThan(SECONDARY_INK_ALPHA)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `the time is set apart from the title it precedes`() {
|
||||||
|
val label = inlineTimeLabel("09:05", "Standup", Color.Black)
|
||||||
|
assertThat(label.text).isEqualTo("09:05 Standup")
|
||||||
|
// Only the time is restyled: the title keeps the surface's own label
|
||||||
|
// style, 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)
|
||||||
|
assertThat(spans.single().item.letterSpacing).isEqualTo(0.sp)
|
||||||
|
assertThat(spans.single().item.color).isEqualTo(Color.Black)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a chip without a time is styled title and nothing else`() {
|
||||||
|
val label = inlineTimeLabel(null, "Standup", Color.Black)
|
||||||
|
assertThat(label.text).isEqualTo("Standup")
|
||||||
|
assertThat(label.spanStyles).isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
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.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
|
||||||
@@ -15,7 +12,8 @@ import kotlinx.datetime.toInstant
|
|||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
/** Which month chips carry a start time, and how it reads (#219). */
|
/** Which month chips carry a start time, and how it reads (#219).
|
||||||
|
* How it is *set* against the title lives in `EventTimeStyleTest`. */
|
||||||
class MonthChipTimeTest {
|
class MonthChipTimeTest {
|
||||||
|
|
||||||
private val zone = TimeZone.UTC
|
private val zone = TimeZone.UTC
|
||||||
@@ -58,26 +56,6 @@ 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 = 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).
|
|
||||||
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 = inlineTimeLabel(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)
|
||||||
|
|||||||
Reference in New Issue
Block a user