Open timelines on the visible hours from other views (#322)

This commit is contained in:
2026-09-30 18:53:49 +02:00
parent eaa938277b
commit a66227bd20
2 changed files with 26 additions and 7 deletions
@@ -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,
@@ -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