diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/TimelineActions.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/TimelineActions.kt new file mode 100644 index 00000000..2a0e69c5 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/TimelineActions.kt @@ -0,0 +1,57 @@ +package de.jeanlucmakiola.calendula.ui.week + +import androidx.compose.foundation.ScrollState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import de.jeanlucmakiola.calendula.domain.EventInstance +import de.jeanlucmakiola.calendula.ui.common.TimelineDragController +import de.jeanlucmakiola.calendula.ui.common.TimelineDrop +import de.jeanlucmakiola.calendula.ui.common.TimelineResizeDrop +import kotlinx.datetime.LocalDate + +/** + * What every timeline page hands down to its day columns: the shared vertical + * scroll, the drag controller, and the callbacks. One instance per screen, so + * passing it never makes a page recompose. + */ +@Stable +internal class TimelineActions( + val scrollState: ScrollState, + val dragController: TimelineDragController, + val onEventClick: (EventInstance) -> Unit, + val onOpenDay: (LocalDate) -> Unit, + val onCreateAt: (LocalDate, Int) -> Unit, + val onDrop: (TimelineDrop) -> Unit, + val onResize: (TimelineResizeDrop) -> Unit, +) + +/** The callbacks are read through their latest values, so the holder outlives them. */ +@Composable +internal fun rememberTimelineActions( + scrollState: ScrollState, + dragController: TimelineDragController, + onEventClick: (EventInstance) -> Unit, + onOpenDay: (LocalDate) -> Unit, + onCreateAt: (LocalDate, Int) -> Unit, + onDrop: (TimelineDrop) -> Unit, + onResize: (TimelineResizeDrop) -> Unit, +): TimelineActions { + val eventClick = rememberUpdatedState(onEventClick) + val openDay = rememberUpdatedState(onOpenDay) + val createAt = rememberUpdatedState(onCreateAt) + val drop = rememberUpdatedState(onDrop) + val resize = rememberUpdatedState(onResize) + return remember(scrollState, dragController) { + TimelineActions( + scrollState = scrollState, + dragController = dragController, + onEventClick = { eventClick.value(it) }, + onOpenDay = { openDay.value(it) }, + onCreateAt = { date, minutes -> createAt.value(date, minutes) }, + onDrop = { drop.value(it) }, + onResize = { resize.value(it) }, + ) + } +} diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt index 2097fbd9..6eaa28e0 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt @@ -39,7 +39,6 @@ import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.ScrollState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -516,7 +515,7 @@ private fun PagedTimelineScreen( onRetry = viewModel::goToToday, onEventClick = onEventClick, onOpenDay = onOpenDay, - onCreateAt = { d, minutes -> onCreateEvent(d, minutes) }, + onCreateAt = onCreateEvent, onCreateDraft = onCreateDraft, draftMinutes = draftMinutes, modifier = Modifier @@ -648,6 +647,16 @@ private fun WeekContent( } } + val actions = rememberTimelineActions( + scrollState = scrollState, + dragController = dragController, + onEventClick = openEvent, + onOpenDay = onOpenDay, + onCreateAt = onCreateAt, + onDrop = onDrop, + onResize = onResize, + ) + Box(modifier = modifier) { CompositionLocalProvider(LocalOpeningTimeline provides timelineScroll) { // Loading and failure concern the provider as a whole, so they stand in @@ -663,17 +672,7 @@ private fun WeekContent( WeekUiState.Loading -> WeekLoading(dayCount = grid.dayCount) is WeekUiState.Failure -> CalendarFailure(reason = s.reason, onRetry = onRetry) is WeekUiState.Success -> if (morph != null) { - TimelineMorphFrame( - morph = morph, - today = today, - scrollState = scrollState, - dragController = dragController, - onEventClick = openEvent, - onOpenDay = onOpenDay, - onCreateAt = onCreateAt, - onDrop = onDrop, - onResize = onResize, - ) + TimelineMorphFrame(morph = morph, today = today, actions = actions) } else WeekPager( pagerState = pagerState, grid = grid, @@ -681,13 +680,7 @@ private fun WeekContent( span = span, today = today, initialStripHeight = handoffStrip ?: s.allDayStripHeight(), - scrollState = scrollState, - dragController = dragController, - onEventClick = openEvent, - onOpenDay = onOpenDay, - onCreateAt = onCreateAt, - onDrop = onDrop, - onResize = onResize, + actions = actions, ) } } @@ -708,22 +701,16 @@ private fun WeekPager( span: (DaySpan) -> StateFlow, today: LocalDate, initialStripHeight: Dp, - scrollState: ScrollState, - dragController: TimelineDragController, - onEventClick: (EventInstance) -> Unit, - onOpenDay: (LocalDate) -> Unit, - onCreateAt: (LocalDate, Int) -> Unit, - onDrop: (TimelineDrop) -> Unit, - onResize: (TimelineResizeDrop) -> Unit, + actions: TimelineActions, ) { + val dragController = actions.dragController val stripHeights = remember { mutableStateMapOf() } val allDayHeight = pagedHeight(pagerState, stripHeights, initialStripHeight) WeekFrame( weekStart = grid.startForPage(pagerState.currentPage), showWeekNumber = kind.showWeekNumber, allDayHeight = allDayHeight, - scrollState = scrollState, - dragController = dragController, + actions = actions, ) { HorizontalPager( state = pagerState, @@ -756,14 +743,8 @@ private fun WeekPager( }, headerBias = kind.headerBias, allDayHeight = allDayHeight, - scrollState = scrollState, - dragController = dragController, + actions = actions, active = page == pagerState.currentPage, - onEventClick = onEventClick, - onOpenDay = onOpenDay, - onCreateAt = onCreateAt, - onDrop = onDrop, - onResize = onResize, ) } } @@ -792,13 +773,7 @@ private fun emptySpan(span: DaySpan, today: LocalDate): WeekUiState.Success { private fun TimelineMorphFrame( morph: TimelineMorph, today: LocalDate, - scrollState: ScrollState, - dragController: TimelineDragController, - onEventClick: (EventInstance) -> Unit, - onOpenDay: (LocalDate) -> Unit, - onCreateAt: (LocalDate, Int) -> Unit, - onDrop: (TimelineDrop) -> Unit, - onResize: (TimelineResizeDrop) -> Unit, + actions: TimelineActions, ) { val fromState by morph.fromState.collectAsStateWithLifecycle() val toState by morph.toState.collectAsStateWithLifecycle() @@ -836,8 +811,7 @@ private fun TimelineMorphFrame( allDayHeight = fromStrip, animatedAllDayHeight = animatedStrip, weekNumberAlpha = badgeAlpha, - scrollState = scrollState, - dragController = dragController, + actions = actions, ) { TimelinePageContent( geometry = geometry, @@ -847,14 +821,8 @@ private fun TimelineMorphFrame( headerBias = headerBias, allDayHeight = fromStrip, animatedAllDayHeight = animatedStrip, - scrollState = scrollState, - dragController = dragController, + actions = actions, active = false, - onEventClick = onEventClick, - onOpenDay = onOpenDay, - onCreateAt = onCreateAt, - onDrop = onDrop, - onResize = onResize, ) } } @@ -888,8 +856,7 @@ private fun WeekFrame( allDayHeight: Dp, animatedAllDayHeight: (() -> Dp)? = null, weekNumberAlpha: () -> Float = { 1f }, - scrollState: ScrollState, - dragController: TimelineDragController, + actions: TimelineActions, pages: @Composable () -> Unit, ) { val gutterFullHeight = remember(animatedAllDayHeight) { intArrayOf(-1) } @@ -923,10 +890,10 @@ private fun WeekFrame( else maxHeight val hourHeight = zoom.scale.hourHeight(fitHeight) HourGutter( - scrollState = scrollState, + scrollState = actions.scrollState, hourHeight = hourHeight, - dragController = dragController, - modifier = rememberTimelinePinchZoom(scrollState, fitHeight, hourHeight, zoom), + dragController = actions.dragController, + modifier = rememberTimelinePinchZoom(actions.scrollState, fitHeight, hourHeight, zoom), ) } } @@ -941,34 +908,21 @@ internal fun WeekSuccess( state: WeekUiState.Success, showWeekNumber: Boolean = true, headerBias: Float = 0f, - scrollState: ScrollState, allDayHeight: Dp, - dragController: TimelineDragController, - onEventClick: (EventInstance) -> Unit, - onOpenDay: (LocalDate) -> Unit, - onCreateAt: (LocalDate, Int) -> Unit, - onDrop: (TimelineDrop) -> Unit, - onResize: (TimelineResizeDrop) -> Unit, + actions: TimelineActions, ) { WeekFrame( weekStart = state.weekStart, showWeekNumber = showWeekNumber, allDayHeight = allDayHeight, - scrollState = scrollState, - dragController = dragController, + actions = actions, ) { WeekPage( state = state, headerBias = headerBias, allDayHeight = allDayHeight, - scrollState = scrollState, - dragController = dragController, + actions = actions, active = true, - onEventClick = onEventClick, - onOpenDay = onOpenDay, - onCreateAt = onCreateAt, - onDrop = onDrop, - onResize = onResize, ) } } @@ -984,14 +938,8 @@ private fun WeekPage( state: WeekUiState.Success, headerBias: Float, allDayHeight: Dp, - scrollState: ScrollState, - dragController: TimelineDragController, + actions: TimelineActions, active: Boolean, - onEventClick: (EventInstance) -> Unit, - onOpenDay: (LocalDate) -> Unit, - onCreateAt: (LocalDate, Int) -> Unit, - onDrop: (TimelineDrop) -> Unit, - onResize: (TimelineResizeDrop) -> Unit, ) { val geometry = remember(state.weekStart, state.days.size) { ColumnGeometry.of(DaySpan(state.weekStart, state.days.size)) @@ -1004,14 +952,8 @@ private fun WeekPage( bars = bars, headerBias = headerBias, allDayHeight = allDayHeight, - scrollState = scrollState, - dragController = dragController, + actions = actions, active = active, - onEventClick = onEventClick, - onOpenDay = onOpenDay, - onCreateAt = onCreateAt, - onDrop = onDrop, - onResize = onResize, ) } @@ -1028,14 +970,8 @@ private fun TimelinePageContent( headerBias: Float, allDayHeight: Dp, animatedAllDayHeight: (() -> Dp)? = null, - scrollState: ScrollState, - dragController: TimelineDragController, + actions: TimelineActions, active: Boolean, - onEventClick: (EventInstance) -> Unit, - onOpenDay: (LocalDate) -> Unit, - onCreateAt: (LocalDate, Int) -> Unit, - onDrop: (TimelineDrop) -> Unit, - onResize: (TimelineResizeDrop) -> Unit, ) { Column(modifier = Modifier.fillMaxSize()) { Column( @@ -1043,14 +979,14 @@ private fun TimelinePageContent( .fillMaxWidth() .background(MaterialTheme.colorScheme.surface), ) { - WeekDayHeader(geometry = geometry, today = today, bias = headerBias, onOpenDay = onOpenDay) + WeekDayHeader(geometry = geometry, today = today, bias = headerBias, onOpenDay = actions.onOpenDay) // This strip is painted on `surface`, so an outlined chip has to // fill with that and not with the columns' container (#230). CompositionLocalProvider( LocalChipGround provides MaterialTheme.colorScheme.surface, ) { AllDayStrip(geometry = geometry, bars = bars, height = allDayHeight, - animatedHeight = animatedAllDayHeight, onEventClick = onEventClick) + animatedHeight = animatedAllDayHeight, onEventClick = actions.onEventClick) } } Spacer(Modifier.height(TIMELINE_TOP_GAP)) @@ -1060,13 +996,8 @@ private fun TimelinePageContent( timedFor = timedFor, allDayHeight = allDayHeight, animatedAllDayHeight = animatedAllDayHeight, - scrollState = scrollState, - dragController = dragController, + actions = actions, active = active, - onEventClick = onEventClick, - onCreateAt = onCreateAt, - onDrop = onDrop, - onResize = onResize, ) } } @@ -1290,14 +1221,11 @@ private fun Timeline( timedFor: (LocalDate) -> List, allDayHeight: Dp, animatedAllDayHeight: (() -> Dp)? = null, - scrollState: ScrollState, - dragController: TimelineDragController, + actions: TimelineActions, active: Boolean, - onEventClick: (EventInstance) -> Unit, - onCreateAt: (LocalDate, Int) -> Unit, - onDrop: (TimelineDrop) -> Unit, - onResize: (TimelineResizeDrop) -> Unit, ) { + val scrollState = actions.scrollState + val dragController = actions.dragController val fullViewportHeight = remember(geometry) { intArrayOf(-1) } val dark = LocalFloretDarkTheme.current val zoom = LocalTimelineZoom.current @@ -1390,12 +1318,7 @@ private fun Timeline( columnWidth = columnWidth, visibleTop = visibleTop, viewportHeight = visibleViewport, - dragController = dragController, - onEventClick = onEventClick, - onCreateAt = onCreateAt, - onDrop = onDrop, - onResize = onResize, - scrollState = scrollState, + actions = actions, modifier = Modifier.fillMaxHeight(), ) } @@ -1413,14 +1336,11 @@ private fun DayColumnCard( columnWidth: Dp, visibleTop: () -> Float, viewportHeight: () -> Float, - dragController: TimelineDragController, - onEventClick: (EventInstance) -> Unit, - onCreateAt: (LocalDate, Int) -> Unit, - onDrop: (TimelineDrop) -> Unit, - onResize: (TimelineResizeDrop) -> Unit, - scrollState: ScrollState, + actions: TimelineActions, modifier: Modifier = Modifier, ) { + val scrollState = actions.scrollState + val dragController = actions.dragController val density = LocalDensity.current val hourPx = with(density) { hourHeight.toPx() } val morphing = LocalViewMorph.current != null @@ -1438,13 +1358,13 @@ private fun DayColumnCard( } // The tap detector outlives the composition that installed it — a pinch or a // Settings change moves the hour height without restarting it — so it reads - // the height and the callback through state handles instead of capturing - // them, or taps land on the scale the column had before the zoom (#148). + // the height through a state handle instead of capturing it, or taps land on + // the scale the column had before the zoom (#148). The actions read their + // callbacks' latest values themselves. val currentHourPx = rememberUpdatedState(hourPx) - val currentOnCreateAt = rememberUpdatedState(onCreateAt) val edges = rememberOutOfViewEdges(blocks, scrollState, hourPx) val currentEdges = rememberUpdatedState(edges) - val draftSurface = rememberDraftSurface(date, edges, hourPx, dragController, onDrop) + val draftSurface = rememberDraftSurface(date, edges, hourPx, dragController, actions.onDrop) Card( // Plain rectangular columns — the soft corners come from the outer // rounded scroll viewport, so inner rounding would look odd at the edges. @@ -1478,7 +1398,7 @@ private fun DayColumnCard( return@detectTapGestures } if (currentEdges.value.tap(offset.y)) return@detectTapGestures - currentOnCreateAt.value( + actions.onCreateAt( date, tappedMinuteOfDay(offset.y, currentHourPx.value), ) @@ -1538,9 +1458,9 @@ private fun DayColumnCard( cuts = cuts, topInset = topInset, dragController = dragController, - onClick = { onEventClick(block.event) }, - onDrop = onDrop, - onResize = onResize, + onClick = { actions.onEventClick(block.event) }, + onDrop = actions.onDrop, + onResize = actions.onResize, modifier = Modifier .laneSlot(place) .offset(y = place.y + topInset) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekViewPreview.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekViewPreview.kt index d9790ce3..e7d87a0e 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekViewPreview.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekViewPreview.kt @@ -52,14 +52,16 @@ internal fun WeekViewPreview( state = state, showWeekNumber = dayCount == 7, headerBias = if (dayCount == 1) TimelineKind.Day.headerBias else 0f, - scrollState = scrollState, allDayHeight = state.allDayStripHeight(), - dragController = rememberTimelineDragController(), - onEventClick = {}, - onOpenDay = {}, - onCreateAt = { _, _ -> }, - onDrop = {}, - onResize = {}, + actions = rememberTimelineActions( + scrollState = scrollState, + dragController = rememberTimelineDragController(), + onEventClick = {}, + onOpenDay = {}, + onCreateAt = { _, _ -> }, + onDrop = {}, + onResize = {}, + ), ) } }