Show today's date in the toolbar today button (#282)

The jump-to-today action in the app bar drew `Icons.Default.Today`, a generic calendar glyph. It now draws the current day number inside an outlined rounded box, so the bar tells you what day it is as well as taking you there. Shared by all four calendar views, so it changes everywhere at once.

- The box fills the same 24dp icon slot `AppBarSpacing` measures its trailing insets from (`IconSize` went from private to internal so the glyph and the spacing math cannot drift apart). No spacing value changed.
- Outlined rather than the month grid's filled circle: that circle means "this cell is today", and a filled accent here would sit heavier than the stroke icons beside it.
- The day comes from `rememberCurrentMinute()` through a `derivedStateOf`, so the button rolls over at midnight — and across a timezone change — while recomposing only when the number actually changes, not on every minute tick.
- The content description is unchanged, still the sentence "Go to today".

### Also here

A review of the branch turned up a loose end in #113, which this branch is merged on top of: the day and week **loading skeletons** still painted one solid `surfaceContainer` column. With the hour grid on that is no longer what a loaded column looks like — the column recedes to `surface` and the container colour moves onto the cells — so the skeleton flashed a solid block into a gapped grid on arrival, the exact resize its own comment promises not to do. Both skeletons now read `LocalShowHourGrid` and draw the same cells.

### Deviation from the issue

The issue asked to watch the width at large font scales. Rather than measuring and adapting, the number is held at a fixed size — `12.dp` converted to sp at draw time, so it ignores the font scale. It is an icon glyph sharing a bar with stroke icons, a growing two-digit day is exactly the width pressure #165 measured, and screen readers get the meaning from the content description either way. A small number is also strictly more than the old icon said, never less.

Closes #220

Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/282
This commit is contained in:
Jean-Luc Makiola
2026-09-07 20:37:47 +02:00
co-authored by makiolaj
parent b0d86af959
commit c2062da5d8
5 changed files with 96 additions and 13 deletions
+6
View File
@@ -32,6 +32,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
three-letter form and the day view drops the weekday, rather than trailing off
mid-word at large font sizes. The view switcher, the title and the agenda's
range bar also line up with the grid underneath them ([#165]).
- The jump-to-today button in the toolbar now shows today's date. It drew a
generic calendar icon, which told you nothing you didn't already know from
tapping it; it now carries the current day number in an outlined box, so the
bar says what day it is as well as taking you there. It rolls over at midnight
on its own ([#220]).
### Fixed
- **A long location no longer runs off the edge of its field.** The location on
@@ -1611,3 +1616,4 @@ automatically, with zero telemetry and no internet permission.
[#253]: https://codeberg.org/jlmakiola/calendula/issues/253
[#273]: https://codeberg.org/jlmakiola/calendula/issues/273
[#113]: https://codeberg.org/jlmakiola/calendula/issues/113
[#220]: https://codeberg.org/jlmakiola/calendula/issues/220
@@ -26,7 +26,8 @@ object AppBarSpacing {
private val IconButtonSize = 48.dp
private val IconSize = 24.dp
/** M3's icon slot inside an icon button; the today glyph fills the same box. */
internal val IconSize = 24.dp
/**
* End padding for a container-backed control that ends the bar, measured to
@@ -1,26 +1,81 @@
package de.jeanlucmakiola.calendula.ui.common
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Today
import androidx.compose.material3.Icon
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import de.jeanlucmakiola.calendula.R
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
private val GlyphShape = RoundedCornerShape(7.dp)
private val GlyphBorder = 1.5.dp
/**
* Height of the day number. Expressed in dp and converted to sp at draw time so
* it does not follow the font scale: this is an icon glyph sharing the bar with
* stroke icons, and a two-digit day that grew with the text would push the bar
* past the width #165 measured it to. The button's content description carries
* the meaning for screen readers, so nothing is lost by holding it still.
*/
private val GlyphTextHeight = 12.dp
/**
* The top-bar "jump to today" icon button, shared by every calendar view's app
* bar (#60). Shown in place of the fade-in FAB pill when the user moves the
* today control into the toolbar; renders nothing when [show] is false, so it
* drops straight into an app bar's `actions` slot without a wrapping condition.
*
* The glyph is the current day number in an outlined box rather than a generic
* calendar icon (#220), so the bar also says what day it is. Outlined rather
* than the month grid's filled circle, which reads as "this cell is today" and
* would sit heavier here than the stroke icons beside it.
*/
@Composable
fun TodayAction(show: Boolean, onToday: () -> Unit) {
if (!show) return
IconButton(onClick = onToday) {
Icon(
imageVector = Icons.Default.Today,
contentDescription = stringResource(R.string.today_jump_action),
)
val now = rememberCurrentMinute()
// Only the day number is read, so the bar recomposes at midnight rather
// than on every minute tick.
val day by remember {
derivedStateOf { now.value.toLocalDateTime(TimeZone.currentSystemDefault()).date.day }
}
val description = stringResource(R.string.today_jump_action)
IconButton(
onClick = onToday,
modifier = Modifier.semantics { contentDescription = description },
) {
val textSize = with(LocalDensity.current) { GlyphTextHeight.toSp() }
Box(
modifier = Modifier
.size(AppBarSpacing.IconSize)
.border(GlyphBorder, LocalContentColor.current, GlyphShape),
contentAlignment = Alignment.Center,
) {
Text(
text = day.toString(),
fontSize = textSize,
lineHeight = textSize,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = LocalContentColor.current,
)
}
}
}
@@ -861,7 +861,12 @@ private fun DayLoading() {
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
// Same scale resolution as the loaded timeline, so the skeleton's column
// doesn't resize the moment the real day arrives.
val totalHeight = scale.hourHeight(maxHeight) * 24
val hourHeight = scale.hourHeight(maxHeight)
val totalHeight = hourHeight * 24
// The skeleton wears the loaded column's own ground, so the arrival of
// the real day doesn't flash a solid block into a gapped grid.
val showHourGrid = LocalShowHourGrid.current
val hourPx = with(LocalDensity.current) { hourHeight.toPx() }
Row(
modifier = Modifier
.fillMaxSize()
@@ -874,7 +879,11 @@ private fun DayLoading() {
.weight(1f)
.height(totalHeight)
.padding(horizontal = 2.dp)
.background(MaterialTheme.colorScheme.surfaceContainer),
.background(
if (showHourGrid) MaterialTheme.colorScheme.surface
else MaterialTheme.colorScheme.surfaceContainer,
)
.hourGridCells(showHourGrid, hourPx, MaterialTheme.colorScheme.surfaceContainer),
)
}
}
@@ -1042,7 +1042,14 @@ private fun WeekLoading() {
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
// Same scale resolution as the loaded timeline, so the skeleton's
// columns don't resize the moment the real week arrives.
val totalHeight = scale.hourHeight(maxHeight) * 24
val hourHeight = scale.hourHeight(maxHeight)
val totalHeight = hourHeight * 24
// The skeleton wears the loaded columns' own ground, so the arrival
// of the real week doesn't flash solid blocks into a gapped grid.
val showHourGrid = LocalShowHourGrid.current
val hourPx = with(LocalDensity.current) { hourHeight.toPx() }
val columnGround = if (showHourGrid) MaterialTheme.colorScheme.surface
else MaterialTheme.colorScheme.surfaceContainer
Row(
modifier = Modifier
.fillMaxSize()
@@ -1056,7 +1063,12 @@ private fun WeekLoading() {
.weight(1f)
.height(totalHeight)
.padding(horizontal = 2.dp)
.background(MaterialTheme.colorScheme.surfaceContainer),
.background(columnGround)
.hourGridCells(
showHourGrid,
hourPx,
MaterialTheme.colorScheme.surfaceContainer,
),
)
}
}