From 7fb01ab8d152343057cd216a00144b6818106525 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 20 Jul 2026 20:43:21 +0200 Subject: [PATCH] fix(month): fade the selection outline with the page it belongs to (#53) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sending the outline to the shared-transition overlay stopped it hiding behind its own cell, but it also lifted it out of the layer AnimatedContent fades the outgoing page with — so it stopped fading at all. It stood at full strength for the whole expand or collapse and then blinked out at the end, which is the opposite half of the same bug. The fade is reapplied on the lifted content's own layer, so it travels with it. That belongs in morphOverlay rather than at the call site: anything sent to the overlay loses the parent's fade the same way. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../calendula/ui/month/MonthMorph.kt | 19 ++++++++++++++++++- 1 file changed, 18 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 7dcf4c0..9a8e909 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 @@ -8,6 +8,7 @@ import androidx.compose.animation.fadeOut import androidx.compose.runtime.Composable import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.Modifier +import de.jeanlucmakiola.calendula.ui.common.rememberCalendarFadeSpec import kotlinx.datetime.LocalDate /** @@ -85,14 +86,30 @@ internal fun Modifier.morphElement(key: MonthMorphKey): Modifier { * The content keeps its own layout bounds; only where it is *painted* changes. So * the outline still draws at the size the collapsed cell actually is, which is * the whole reason it was left untagged. + * + * Lifting content out of the regular tree also lifts it out of the enter/exit + * layer [AnimatedContent][androidx.compose.animation.AnimatedContent] fades the + * outgoing page with, so it no longer fades at all — it stood at full strength + * for the whole transition and then blinked out at the end of it. The fade is + * therefore reapplied here, on the lifted content's own layer, so it travels with + * it. Anything sent to the overlay needs this, which is why it lives in the + * helper rather than at the call site. */ @OptIn(ExperimentalSharedTransitionApi::class) @Composable internal fun Modifier.morphOverlay(zIndex: Float = 1f): Modifier { val morph = LocalMonthMorph.current ?: return this - return with(morph.shared) { + val fadeSpec = rememberCalendarFadeSpec() + val lifted = with(morph.shared) { this@morphOverlay.renderInSharedTransitionScopeOverlay(zIndexInOverlay = zIndex) } + return with(morph.visibility) { + lifted.animateEnterExit( + enter = fadeIn(fadeSpec), + exit = fadeOut(fadeSpec), + label = "morph-overlay-fade", + ) + } } /**