From a66227bd20fd6b1a3b92d30126b70cc812b2f9b1 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 30 Sep 2026 18:53:49 +0200 Subject: [PATCH] Open timelines on the visible hours from other views (#322) --- .../calendula/ui/CalendarHost.kt | 9 +++++++ .../calendula/ui/common/TimelineScroll.kt | 24 +++++++++++++------ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt index 6dde434..2fafe09 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt @@ -66,6 +66,7 @@ import de.jeanlucmakiola.calendula.ui.common.ViewFocusSaver import de.jeanlucmakiola.calendula.ui.common.LocalViewMorph import de.jeanlucmakiola.calendula.ui.common.RescheduleViewModel import de.jeanlucmakiola.calendula.ui.common.TimelineScrollMemory +import de.jeanlucmakiola.calendula.ui.common.LocalTimelineZoom import de.jeanlucmakiola.calendula.ui.common.ViewMorphScope import de.jeanlucmakiola.calendula.ui.common.rememberCalendarFadeSpec import de.jeanlucmakiola.calendula.ui.common.rememberViewMorphBoundsTransform @@ -446,6 +447,14 @@ fun CalendarHost( val topBarBottom = TopAppBarDefaults.windowInsets.getTop(density) + with(density) { TopAppBarDefaults.TopAppBarExpandedHeight.toPx() } val timelineScroll = remember { TimelineScrollMemory() } + // A timeline reached from another screen opens on the visible hours + // (#322); only a switch between timeline views carries the scroll over. + val timelineZoom = LocalTimelineZoom.current + LaunchedEffect(view.screen) { + if (view.screen != ViewScreen.Timeline && timelineZoom.hours.enabled) { + timelineScroll.midnightY = null + } + } CompositionLocalProvider( LocalEventMove provides moveScope, LocalTimelineScroll provides timelineScroll, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineScroll.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineScroll.kt index eaf19ff..7282db7 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineScroll.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineScroll.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withTimeoutOrNull import kotlin.math.roundToInt /** @@ -81,14 +82,20 @@ fun rememberTimelineScrollState(timelineTop: Dp): TimelineScroll { snapshotFlow { scrollState.maxValue }.first { it > 0 } scrollState.scrollTo(scrollState.maxValue / 2) } else { - val (target, hourHeight) = snapshotFlow { - val viewport = scrollState.viewportSize - val hourHeight = zoom.scale.hourHeight(with(density) { viewport.toDp() }) - val hourPx = with(density) { hourHeight.toPx() } - startHourScroll(scrollState.maxValue, viewport, startHour, hourPx)?.let { it to hourHeight } - }.filterNotNull().first() - scrollState.scrollTo(target) + // Fitted to the scroll viewport itself: the layout resolves scales + // against the viewport plus the all-day strip, which would push the + // end hour below the screen. + val viewport = snapshotFlow { scrollState.viewportSize }.first { it > 0 } + val hourHeight = zoom.scale.hourHeight(with(density) { viewport.toDp() }) zoom.holdFit(hourHeight) + val hourPx = with(density) { hourHeight.toPx() } + val target = withTimeoutOrNull(FIT_LAYOUT_TIMEOUT_MILLIS) { + snapshotFlow { + startHourScroll(scrollState.maxValue, scrollState.viewportSize, startHour, hourPx) + }.filterNotNull().first() + } ?: (startHour * (scrollState.maxValue + scrollState.viewportSize) / 24) + .coerceIn(0, scrollState.maxValue.coerceAtLeast(0)) + scrollState.scrollTo(target) } scroll.noonPending = false } @@ -99,3 +106,6 @@ fun rememberTimelineScrollState(timelineTop: Dp): TimelineScroll { } return scroll } + +/** How long an opening timeline waits to be laid out at its fitted hours before scrolling anyway. */ +private const val FIT_LAYOUT_TIMEOUT_MILLIS = 500L