From f7d88e55d0b7a06f2222bf5924a333ae2bf21193 Mon Sep 17 00:00:00 2001 From: SalimMcPicsou <185490663+SalimMc@users.noreply.github.com> Date: Fri, 30 Oct 2026 18:32:11 +0100 Subject: [PATCH] Lay the month calendar like pool tiles Days become rounded square tiles on a light grout, and a day without entries keeps a faint tile so the month still reads as a tiled floor. The legend uses the same tiles. Co-Authored-By: Claude Opus 5.5 --- .../ui/history/heatmap/HeatmapLegend.kt | 18 +++-- .../ui/history/heatmap/MonthCalendar.kt | 24 +++++-- .../ui/history/heatmap/PoolTileColorsTest.kt | 71 +++++++++++++++++++ 3 files changed, 102 insertions(+), 11 deletions(-) create mode 100644 app/src/test/java/com/github/se/pooltrack/ui/history/heatmap/PoolTileColorsTest.kt diff --git a/app/src/main/java/com/github/se/pooltrack/ui/history/heatmap/HeatmapLegend.kt b/app/src/main/java/com/github/se/pooltrack/ui/history/heatmap/HeatmapLegend.kt index 6998974..80122a7 100644 --- a/app/src/main/java/com/github/se/pooltrack/ui/history/heatmap/HeatmapLegend.kt +++ b/app/src/main/java/com/github/se/pooltrack/ui/history/heatmap/HeatmapLegend.kt @@ -5,7 +5,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -41,7 +41,16 @@ fun heatmapColor(level: Int, palette: HeatmapPalette = DayPalette): Color = if (level <= 0) Color.Transparent else MaterialTheme.colorScheme.primary.copy(alpha = levelAlpha(level, palette)) -/** The "Less, five dots, More" key explaining the colors. */ +/** The shape of a day in the calendars: a pool tile with softened corners. */ +val PoolTileShape = RoundedCornerShape(6.dp) + +/** A day without entries: a bare tile, barely there, so the month still reads as a tiled floor. */ +@Composable fun emptyTileColor(): Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f) + +/** The lines between the tiles of a calendar, like the grout between a pool's tiles. */ +@Composable fun groutColor(): Color = MaterialTheme.colorScheme.primary.copy(alpha = 0.10f) + +/** The "Less, five tiles, More" key explaining the colors. */ @Composable fun HeatmapLegend(modifier: Modifier = Modifier) { Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { @@ -56,9 +65,8 @@ fun HeatmapLegend(modifier: Modifier = Modifier) { Modifier.padding(start = 4.dp) .size(12.dp) .background( - if (level == 0) MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f) - else heatmapColor(level), - CircleShape, + if (level == 0) emptyTileColor() else heatmapColor(level), + RoundedCornerShape(3.dp), ) ) } diff --git a/app/src/main/java/com/github/se/pooltrack/ui/history/heatmap/MonthCalendar.kt b/app/src/main/java/com/github/se/pooltrack/ui/history/heatmap/MonthCalendar.kt index 2ded563..d9cef99 100644 --- a/app/src/main/java/com/github/se/pooltrack/ui/history/heatmap/MonthCalendar.kt +++ b/app/src/main/java/com/github/se/pooltrack/ui/history/heatmap/MonthCalendar.kt @@ -13,7 +13,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight @@ -53,8 +53,9 @@ object MonthCalendarTestTags { /** * A month calendar in the spirit of the iPhone's: a title with previous/next buttons, a row of - * weekday initials and a grid of round days that fills the width. Days with entries are filled, - * darker for more entries. Swipe sideways or use the buttons to change month. + * weekday initials and a grid of days that fills the width, laid like the tiles of a pool. Days + * with entries are filled, deeper blue for more entries. Swipe sideways or use the buttons to + * change month. * * @param months The months that can be shown, oldest first; it opens on the last one. * @param countsByDay Entries per day, see [entryCountsByDay]. @@ -169,7 +170,14 @@ fun MonthCalendar( remember(months[page], countsByDay, today, firstDayOfWeek, maxCount) { buildMonthGrid(months[page], countsByDay, today, firstDayOfWeek, maxCount) } - Column(modifier = Modifier.fillMaxWidth()) { + // The light lines between the tiles, like the grout of a pool floor. + Column( + modifier = + Modifier.fillMaxWidth() + .padding(top = 4.dp) + .background(groutColor(), RoundedCornerShape(10.dp)) + .padding(3.dp), + ) { grid.weeks.forEach { week -> Row(modifier = Modifier.fillMaxWidth()) { (0 until 7).forEach { column -> @@ -221,8 +229,12 @@ internal fun DayCell( modifier = Modifier.fillMaxWidth() .aspectRatio(1f) - .background(heatmapColor(day.level), CircleShape) - .then(if (ring != null) Modifier.border(ring, CircleShape) else Modifier) + .background( + if (day.level == 0 && !day.isFuture) emptyTileColor() + else heatmapColor(day.level), + PoolTileShape, + ) + .then(if (ring != null) Modifier.border(ring, PoolTileShape) else Modifier) .then(if (day.isFuture) Modifier else Modifier.clickable(onClick = onClick)) .semantics { contentDescription = description } .testTag(testTag), diff --git a/app/src/test/java/com/github/se/pooltrack/ui/history/heatmap/PoolTileColorsTest.kt b/app/src/test/java/com/github/se/pooltrack/ui/history/heatmap/PoolTileColorsTest.kt new file mode 100644 index 0000000..1f09010 --- /dev/null +++ b/app/src/test/java/com/github/se/pooltrack/ui/history/heatmap/PoolTileColorsTest.kt @@ -0,0 +1,71 @@ +package com.github.se.pooltrack.ui.history.heatmap + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.test.junit4.createComposeRule +import com.github.se.pooltrack.ui.theme.PoolTrackTheme +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class PoolTileColorsTest { + + @get:Rule val composeRule = createComposeRule() + + private data class Resolved( + val scheme: ColorScheme, + val emptyTile: Color, + val grout: Color, + val lightestFill: Color, + ) + + private fun resolve(darkTheme: Boolean): Resolved { + lateinit var resolved: Resolved + composeRule.setContent { + PoolTrackTheme(darkTheme = darkTheme) { + resolved = + Resolved( + MaterialTheme.colorScheme, + emptyTileColor(), + groutColor(), + heatmapColor(1), + ) + } + } + composeRule.waitForIdle() + return resolved + } + + @Test + fun emptyTile_isAFaintVeilOfTheTextColor_inLightTheme() { + val resolved = resolve(darkTheme = false) + + assertEquals(resolved.scheme.onSurface.copy(alpha = 0.06f), resolved.emptyTile) + } + + @Test + fun emptyTile_isAFaintVeilOfTheTextColor_inDarkTheme() { + val resolved = resolve(darkTheme = true) + + assertEquals(resolved.scheme.onSurface.copy(alpha = 0.06f), resolved.emptyTile) + } + + @Test + fun emptyTile_staysFainterThanADayWithOneEntry() { + val resolved = resolve(darkTheme = true) + + assertTrue(resolved.emptyTile.alpha < resolved.lightestFill.alpha) + } + + @Test + fun grout_isALightTintOfThePrimaryColor() { + val resolved = resolve(darkTheme = false) + + assertEquals(resolved.scheme.primary.copy(alpha = 0.10f), resolved.grout) + } +}