Bundle the timeline's shared scroll, drag and callbacks into TimelineActions
This commit is contained in:
@@ -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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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<WeekUiState>,
|
||||
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<Int, Dp>() }
|
||||
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<TimedBlock>,
|
||||
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)
|
||||
|
||||
@@ -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 = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user