From 1e9581528e3b8be59b8f273f3d6ee9ce4893afbd Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 20 Jul 2026 21:40:46 +0200 Subject: [PATCH] fix(month): stop the row clip swallowing marks in mid-flight (#53) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frame-by-frame, the marks were not fading in late — they were invisible for the first half of their journey and then appearing already near the destination, row by row from the top down. A mark's two homes are in different rows: the dot for the 20th sits a third of the way down the compact grid, its bar most of the way down the expanded one. The events box clips to its own row, so a mark arriving there is out of bounds — and undrawn — until it crosses in. The lower the row, the further it travels and the later it showed, which is exactly the order the capture shows them appearing in. Rendering in place rather than in an overlay is what subjects marks to that clip, and that is worth keeping — it is what puts the whole grid in one z-order. So the clip yields instead, and only while pieces are in flight; at rest it still stops a bar spilling into the row below. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../calendula/ui/month/MonthMorph.kt | 19 +++++++++++++++++++ .../calendula/ui/month/MonthScreen.kt | 7 ++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthMorph.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthMorph.kt index 8c61e9f..c8d099c 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthMorph.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthMorph.kt @@ -66,6 +66,25 @@ internal class MonthMorphScope( internal val LocalMonthMorph = compositionLocalOf { null } +/** + * True while the grid is mid-morph, for content that has to stop clipping to let + * the travelling pieces through. + * + * A mark's two homes are in *different rows*: the dot for the 20th sits a third + * of the way down the compact grid, its bar most of the way down the expanded + * one. Clipped to the row it is arriving at, a mark spends the first half of its + * journey outside those bounds and is simply not drawn — so it appears from + * nowhere, part-way through, already near its destination. Rendering in place + * rather than in an overlay is what subjects them to that clip, and is worth + * keeping; the clip is what has to yield, and only while pieces are in flight. + */ +@OptIn(ExperimentalSharedTransitionApi::class) +@Composable +internal fun morphInFlight(): Boolean { + val morph = LocalMonthMorph.current ?: return false + return morph.shared.isTransitionActive +} + /** * Tag content that is *the same thing* on both sides — a background pill, a day * number — so it animates between its two positions and sizes. diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt index 21fab47..faa39ce 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt @@ -1647,6 +1647,7 @@ private fun MonthWeekRow( val dark = isSystemInDarkTheme() val laneCount = (week.spans.maxOfOrNull { it.lane } ?: -1) + 1 val shownLanes = laneCount.coerceAtMost(MAX_EVENT_ROWS) + val morphing = morphInFlight() Row(modifier) { // Optional calendar-week gutter, sized so the seven day columns below @@ -1720,11 +1721,15 @@ private fun MonthWeekRow( // Breathing room between the day number (and today's circle) and the // first event row. Spacer(Modifier.height(DAY_NUMBER_GAP)) + // Clipped at rest so a bar can't spill into the row below, but not + // while a morph is in flight: a mark travels between two different + // rows, and clipping it to the one it is arriving at hides it for + // the whole first half of the journey. See [morphInFlight]. Box( modifier = Modifier .fillMaxWidth() .weight(1f) - .clipToBounds(), + .then(if (morphing) Modifier else Modifier.clipToBounds()), ) { // Spanning bars on their shared lanes. week.spans.filter { it.lane < shownLanes }.forEach { span ->