Resize timeline columns in place when switching views (#184)
This commit is contained in:
@@ -20,6 +20,9 @@ import androidx.compose.runtime.saveable.listSaver
|
|||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.screen
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.ViewScreen
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.layout.positionInWindow
|
import androidx.compose.ui.layout.positionInWindow
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
@@ -53,7 +56,6 @@ import de.jeanlucmakiola.calendula.ui.common.drillToDay
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
||||||
import de.jeanlucmakiola.calendula.ui.common.selectView
|
import de.jeanlucmakiola.calendula.ui.common.selectView
|
||||||
import de.jeanlucmakiola.calendula.ui.common.viewBaseStack
|
import de.jeanlucmakiola.calendula.ui.common.viewBaseStack
|
||||||
import de.jeanlucmakiola.calendula.ui.week.DayScreen
|
|
||||||
import de.jeanlucmakiola.calendula.ui.detail.EventDetailScreen
|
import de.jeanlucmakiola.calendula.ui.detail.EventDetailScreen
|
||||||
import de.jeanlucmakiola.calendula.ui.edit.EventEditScreen
|
import de.jeanlucmakiola.calendula.ui.edit.EventEditScreen
|
||||||
import de.jeanlucmakiola.calendula.ui.edit.ImportSource
|
import de.jeanlucmakiola.calendula.ui.edit.ImportSource
|
||||||
@@ -61,8 +63,7 @@ import de.jeanlucmakiola.calendula.ui.imports.ImportScreen
|
|||||||
import de.jeanlucmakiola.calendula.ui.month.MonthScreen
|
import de.jeanlucmakiola.calendula.ui.month.MonthScreen
|
||||||
import de.jeanlucmakiola.calendula.ui.search.SearchScreen
|
import de.jeanlucmakiola.calendula.ui.search.SearchScreen
|
||||||
import de.jeanlucmakiola.calendula.ui.settings.SettingsScreen
|
import de.jeanlucmakiola.calendula.ui.settings.SettingsScreen
|
||||||
import de.jeanlucmakiola.calendula.ui.week.MultiDayScreen
|
import de.jeanlucmakiola.calendula.ui.week.TimelineScreen
|
||||||
import de.jeanlucmakiola.calendula.ui.week.WeekScreen
|
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.datetime.toLocalDateTime
|
import kotlinx.datetime.toLocalDateTime
|
||||||
@@ -121,6 +122,13 @@ fun CalendarHost(
|
|||||||
mutableStateOf(listOf(defaultView))
|
mutableStateOf(listOf(defaultView))
|
||||||
}
|
}
|
||||||
val view = viewStack.last()
|
val view = viewStack.last()
|
||||||
|
// The timeline view last shown, so the timeline leaving for another screen
|
||||||
|
// keeps drawing the view it was on while it goes.
|
||||||
|
var heldTimeline by rememberSaveable {
|
||||||
|
mutableStateOf(if (view.screen == ViewScreen.Timeline) view else CalendarView.Week)
|
||||||
|
}
|
||||||
|
val timelineView = if (view.screen == ViewScreen.Timeline) view else heldTimeline
|
||||||
|
SideEffect { heldTimeline = timelineView }
|
||||||
val onSelectView: (CalendarView) -> Unit = { viewStack = viewStack.selectView(it) }
|
val onSelectView: (CalendarView) -> Unit = { viewStack = viewStack.selectView(it) }
|
||||||
|
|
||||||
// The date every view opens on and carries along (#184); tapping a day
|
// The date every view opens on and carries along (#184); tapping a day
|
||||||
@@ -400,20 +408,23 @@ fun CalendarHost(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.onGloballyPositioned { morphClip.top = topBarBottom - it.positionInWindow().y },
|
.onGloballyPositioned { morphClip.top = topBarBottom - it.positionInWindow().y },
|
||||||
) {
|
) {
|
||||||
|
// Keyed on the screen, not the view: the day, multi-day and week views
|
||||||
|
// are one timeline that resizes between them itself (#184), so switching
|
||||||
|
// among them must not replay this transition.
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = view,
|
targetState = view.screen,
|
||||||
transitionSpec = { viewSwitch },
|
transitionSpec = { viewSwitch },
|
||||||
label = "view-switch",
|
label = "view-switch",
|
||||||
) { currentView ->
|
) { screen ->
|
||||||
val sharedScope = this@SharedTransitionLayout
|
val sharedScope = this@SharedTransitionLayout
|
||||||
val visibilityScope = this@AnimatedContent
|
val visibilityScope = this@AnimatedContent
|
||||||
val viewMorph = remember(reduceMotion, sharedScope, visibilityScope, morphBounds, morphFade, morphClip) {
|
val viewMorph = remember(reduceMotion, sharedScope, visibilityScope, morphBounds, morphFade, morphClip) {
|
||||||
if (reduceMotion) null else ViewMorphScope(sharedScope, visibilityScope, morphBounds, morphFade, morphClip)
|
if (reduceMotion) null else ViewMorphScope(sharedScope, visibilityScope, morphBounds, morphFade, morphClip)
|
||||||
}
|
}
|
||||||
CompositionLocalProvider(LocalViewMorph provides viewMorph) {
|
CompositionLocalProvider(LocalViewMorph provides viewMorph) {
|
||||||
when (currentView) {
|
when (screen) {
|
||||||
CalendarView.Week -> WeekScreen(
|
ViewScreen.Timeline -> TimelineScreen(
|
||||||
selectedView = currentView,
|
view = timelineView,
|
||||||
onSelectView = onSelectView,
|
onSelectView = onSelectView,
|
||||||
onOpenDay = onOpenDay,
|
onOpenDay = onOpenDay,
|
||||||
onEventClick = onEventClick,
|
onEventClick = onEventClick,
|
||||||
@@ -424,8 +435,8 @@ fun CalendarHost(
|
|||||||
drawerViewOrder = drawerViewOrder,
|
drawerViewOrder = drawerViewOrder,
|
||||||
todayInToolbar = todayInToolbar,
|
todayInToolbar = todayInToolbar,
|
||||||
)
|
)
|
||||||
CalendarView.MultiDay -> MultiDayScreen(
|
ViewScreen.Month -> MonthScreen(
|
||||||
selectedView = currentView,
|
selectedView = CalendarView.Month,
|
||||||
onSelectView = onSelectView,
|
onSelectView = onSelectView,
|
||||||
onOpenDay = onOpenDay,
|
onOpenDay = onOpenDay,
|
||||||
onEventClick = onEventClick,
|
onEventClick = onEventClick,
|
||||||
@@ -436,31 +447,8 @@ fun CalendarHost(
|
|||||||
drawerViewOrder = drawerViewOrder,
|
drawerViewOrder = drawerViewOrder,
|
||||||
todayInToolbar = todayInToolbar,
|
todayInToolbar = todayInToolbar,
|
||||||
)
|
)
|
||||||
CalendarView.Day -> DayScreen(
|
ViewScreen.Agenda -> AgendaScreen(
|
||||||
selectedView = currentView,
|
selectedView = CalendarView.Agenda,
|
||||||
onSelectView = onSelectView,
|
|
||||||
onEventClick = onEventClick,
|
|
||||||
onOpenSettings = onOpenSettings,
|
|
||||||
onOpenSearch = onOpenSearch,
|
|
||||||
onCreateEvent = onCreateEvent,
|
|
||||||
quickSwitchViews = quickSwitchViews,
|
|
||||||
drawerViewOrder = drawerViewOrder,
|
|
||||||
todayInToolbar = todayInToolbar,
|
|
||||||
)
|
|
||||||
CalendarView.Month -> MonthScreen(
|
|
||||||
selectedView = currentView,
|
|
||||||
onSelectView = onSelectView,
|
|
||||||
onOpenDay = onOpenDay,
|
|
||||||
onEventClick = onEventClick,
|
|
||||||
onOpenSettings = onOpenSettings,
|
|
||||||
onOpenSearch = onOpenSearch,
|
|
||||||
onCreateEvent = onCreateEvent,
|
|
||||||
quickSwitchViews = quickSwitchViews,
|
|
||||||
drawerViewOrder = drawerViewOrder,
|
|
||||||
todayInToolbar = todayInToolbar,
|
|
||||||
)
|
|
||||||
CalendarView.Agenda -> AgendaScreen(
|
|
||||||
selectedView = currentView,
|
|
||||||
onSelectView = onSelectView,
|
onSelectView = onSelectView,
|
||||||
onOpenDay = onOpenDay,
|
onOpenDay = onOpenDay,
|
||||||
onEventClick = onEventClick,
|
onEventClick = onEventClick,
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
package de.jeanlucmakiola.calendula.ui.common
|
package de.jeanlucmakiola.calendula.ui.common
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.SizeTransform
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -71,12 +76,24 @@ fun CalendarTitleButton(
|
|||||||
availableWidth = constraints.maxWidth,
|
availableWidth = constraints.maxWidth,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
// Cross-fades, so switching views or paging turns the title over
|
||||||
text = shown,
|
// rather than snapping it (#184).
|
||||||
style = style,
|
val fadeSpec = rememberCalendarFadeSpec()
|
||||||
maxLines = 1,
|
AnimatedContent(
|
||||||
overflow = TextOverflow.Ellipsis,
|
targetState = shown,
|
||||||
)
|
transitionSpec = {
|
||||||
|
(fadeIn(fadeSpec) togetherWith fadeOut(fadeSpec)).using(SizeTransform(clip = false))
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.CenterStart,
|
||||||
|
label = "calendar-title",
|
||||||
|
) { text ->
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = style,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.ArrowDropDown,
|
imageVector = Icons.Default.ArrowDropDown,
|
||||||
|
|||||||
@@ -129,3 +129,13 @@ fun viewBaseStack(default: CalendarView, source: CalendarView): List<CalendarVie
|
|||||||
/** Push the day view as a drill-in over the current stack (no-op if already on it). */
|
/** Push the day view as a drill-in over the current stack (no-op if already on it). */
|
||||||
fun List<CalendarView>.drillToDay(): List<CalendarView> =
|
fun List<CalendarView>.drillToDay(): List<CalendarView> =
|
||||||
if (lastOrNull() == CalendarView.Day) this else this + CalendarView.Day
|
if (lastOrNull() == CalendarView.Day) this else this + CalendarView.Day
|
||||||
|
|
||||||
|
/** The screens the views are drawn by: the day, multi-day and week views share one timeline (#184). */
|
||||||
|
enum class ViewScreen { Month, Timeline, Agenda }
|
||||||
|
|
||||||
|
val CalendarView.screen: ViewScreen
|
||||||
|
get() = when (this) {
|
||||||
|
CalendarView.Month -> ViewScreen.Month
|
||||||
|
CalendarView.Agenda -> ViewScreen.Agenda
|
||||||
|
CalendarView.Week, CalendarView.MultiDay, CalendarView.Day -> ViewScreen.Timeline
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
package de.jeanlucmakiola.calendula.ui.common
|
package de.jeanlucmakiola.calendula.ui.common
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.SizeTransform
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material3.FilledTonalButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
@@ -30,11 +36,23 @@ fun ViewSwitcherPill(
|
|||||||
trailingInset: Dp = AppBarSpacing.ContainerTrailingInset,
|
trailingInset: Dp = AppBarSpacing.ContainerTrailingInset,
|
||||||
) {
|
) {
|
||||||
if (cycle.size < QuickSwitchConfig.MIN_CYCLE) return
|
if (cycle.size < QuickSwitchConfig.MIN_CYCLE) return
|
||||||
|
val fadeSpec = rememberCalendarFadeSpec()
|
||||||
FilledTonalButton(
|
FilledTonalButton(
|
||||||
onClick = onCycle,
|
onClick = onCycle,
|
||||||
shape = MaterialTheme.shapes.large,
|
shape = MaterialTheme.shapes.large,
|
||||||
modifier = modifier.padding(end = trailingInset),
|
modifier = modifier.padding(end = trailingInset),
|
||||||
) {
|
) {
|
||||||
Text(current.label())
|
// The label cross-fades and the pill eases to its width, so cycling
|
||||||
|
// views turns it over rather than snapping it (#184).
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = current.label(),
|
||||||
|
transitionSpec = {
|
||||||
|
(fadeIn(fadeSpec) togetherWith fadeOut(fadeSpec)).using(SizeTransform(clip = false))
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
label = "view-pill",
|
||||||
|
) { label ->
|
||||||
|
Text(label)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.ui.week
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A switch between two timeline views in progress (#184): the page on screen
|
||||||
|
* when it started, the page it ends on, and how far between the two it is.
|
||||||
|
*/
|
||||||
|
@Stable
|
||||||
|
internal class TimelineMorph(
|
||||||
|
val fromKind: TimelineKind,
|
||||||
|
val toKind: TimelineKind,
|
||||||
|
val from: DaySpan,
|
||||||
|
val to: DaySpan,
|
||||||
|
val fromState: StateFlow<WeekUiState>,
|
||||||
|
val toState: StateFlow<WeekUiState>,
|
||||||
|
) {
|
||||||
|
val progress = Animatable(0f)
|
||||||
|
|
||||||
|
/** Where each day's events come from: the page it was on, else the page it goes to. */
|
||||||
|
fun sourceOf(date: LocalDate): Side = if (date in from.start..from.end) Side.From else Side.To
|
||||||
|
|
||||||
|
enum class Side { From, To }
|
||||||
|
}
|
||||||
@@ -68,6 +68,13 @@ import androidx.compose.runtime.snapshotFlow
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.BiasAlignment
|
import androidx.compose.ui.BiasAlignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.LocalViewMorph
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import androidx.compose.runtime.withFrameNanos
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
import de.jeanlucmakiola.calendula.ui.common.laneSlot
|
import de.jeanlucmakiola.calendula.ui.common.laneSlot
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LaneColumn
|
import de.jeanlucmakiola.calendula.ui.common.LaneColumn
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -211,9 +218,13 @@ internal fun WeekUiState.Success.allDayStripHeight(): Dp {
|
|||||||
return ALL_DAY_ROW_HEIGHT * lanes + ALL_DAY_VERTICAL_PADDING * 2
|
return ALL_DAY_ROW_HEIGHT * lanes + ALL_DAY_VERTICAL_PADDING * 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The day, multi-day (#193) and week views: one timeline whose columns resize
|
||||||
|
* in place when switching between them (#184), rather than three screens.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun WeekScreen(
|
fun TimelineScreen(
|
||||||
selectedView: CalendarView,
|
view: CalendarView,
|
||||||
onSelectView: (CalendarView) -> Unit,
|
onSelectView: (CalendarView) -> Unit,
|
||||||
onOpenDay: (LocalDate) -> Unit,
|
onOpenDay: (LocalDate) -> Unit,
|
||||||
onEventClick: (EventInstance) -> Unit,
|
onEventClick: (EventInstance) -> Unit,
|
||||||
@@ -224,84 +235,41 @@ fun WeekScreen(
|
|||||||
drawerViewOrder: List<CalendarView> = IMPLEMENTED_VIEWS,
|
drawerViewOrder: List<CalendarView> = IMPLEMENTED_VIEWS,
|
||||||
todayInToolbar: Boolean = false,
|
todayInToolbar: Boolean = false,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: WeekViewModel = hiltViewModel(),
|
weekViewModel: WeekViewModel = hiltViewModel(),
|
||||||
) = PagedTimelineScreen(
|
multiDayViewModel: MultiDayViewModel = hiltViewModel(),
|
||||||
viewModel = viewModel,
|
dayViewModel: DayViewModel = hiltViewModel(),
|
||||||
kind = TimelineKind.Week,
|
) {
|
||||||
selectedView = selectedView,
|
val viewModels = remember(weekViewModel, multiDayViewModel, dayViewModel) {
|
||||||
onSelectView = onSelectView,
|
TimelineViewModels(weekViewModel, multiDayViewModel, dayViewModel)
|
||||||
onOpenDay = onOpenDay,
|
}
|
||||||
onEventClick = onEventClick,
|
PagedTimelineScreen(
|
||||||
onOpenSettings = onOpenSettings,
|
view = view,
|
||||||
onOpenSearch = onOpenSearch,
|
viewModels = viewModels,
|
||||||
onCreateEvent = onCreateEvent,
|
onSelectView = onSelectView,
|
||||||
quickSwitchViews = quickSwitchViews,
|
onOpenDay = onOpenDay,
|
||||||
drawerViewOrder = drawerViewOrder,
|
onEventClick = onEventClick,
|
||||||
todayInToolbar = todayInToolbar,
|
onOpenSettings = onOpenSettings,
|
||||||
modifier = modifier,
|
onOpenSearch = onOpenSearch,
|
||||||
)
|
onCreateEvent = onCreateEvent,
|
||||||
|
quickSwitchViews = quickSwitchViews,
|
||||||
|
drawerViewOrder = drawerViewOrder,
|
||||||
|
todayInToolbar = todayInToolbar,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** The multi-day view (#193): the week view's timeline over the day count from Settings. */
|
/** Each timeline view's own pages and anchor, held side by side so a switch can show both. */
|
||||||
@Composable
|
internal class TimelineViewModels(
|
||||||
fun MultiDayScreen(
|
private val week: WeekViewModel,
|
||||||
selectedView: CalendarView,
|
private val multiDay: MultiDayViewModel,
|
||||||
onSelectView: (CalendarView) -> Unit,
|
private val day: DayViewModel,
|
||||||
onOpenDay: (LocalDate) -> Unit,
|
) {
|
||||||
onEventClick: (EventInstance) -> Unit,
|
operator fun get(view: CalendarView): PagedTimelineViewModel = when (view) {
|
||||||
onOpenSettings: () -> Unit,
|
CalendarView.MultiDay -> multiDay
|
||||||
onOpenSearch: () -> Unit,
|
CalendarView.Day -> day
|
||||||
onCreateEvent: (LocalDate, Int?) -> Unit,
|
else -> week
|
||||||
quickSwitchViews: List<CalendarView> = IMPLEMENTED_VIEWS,
|
}
|
||||||
drawerViewOrder: List<CalendarView> = IMPLEMENTED_VIEWS,
|
}
|
||||||
todayInToolbar: Boolean = false,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
viewModel: MultiDayViewModel = hiltViewModel(),
|
|
||||||
) = PagedTimelineScreen(
|
|
||||||
viewModel = viewModel,
|
|
||||||
kind = TimelineKind.MultiDay,
|
|
||||||
selectedView = selectedView,
|
|
||||||
onSelectView = onSelectView,
|
|
||||||
onOpenDay = onOpenDay,
|
|
||||||
onEventClick = onEventClick,
|
|
||||||
onOpenSettings = onOpenSettings,
|
|
||||||
onOpenSearch = onOpenSearch,
|
|
||||||
onCreateEvent = onCreateEvent,
|
|
||||||
quickSwitchViews = quickSwitchViews,
|
|
||||||
drawerViewOrder = drawerViewOrder,
|
|
||||||
todayInToolbar = todayInToolbar,
|
|
||||||
modifier = modifier,
|
|
||||||
)
|
|
||||||
|
|
||||||
/** The day view: the week view's timeline one day at a time. */
|
|
||||||
@Composable
|
|
||||||
fun DayScreen(
|
|
||||||
selectedView: CalendarView,
|
|
||||||
onSelectView: (CalendarView) -> Unit,
|
|
||||||
onEventClick: (EventInstance) -> Unit,
|
|
||||||
onOpenSettings: () -> Unit,
|
|
||||||
onOpenSearch: () -> Unit,
|
|
||||||
onCreateEvent: (LocalDate, Int?) -> Unit,
|
|
||||||
quickSwitchViews: List<CalendarView> = IMPLEMENTED_VIEWS,
|
|
||||||
drawerViewOrder: List<CalendarView> = IMPLEMENTED_VIEWS,
|
|
||||||
todayInToolbar: Boolean = false,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
viewModel: DayViewModel = hiltViewModel(),
|
|
||||||
) = PagedTimelineScreen(
|
|
||||||
viewModel = viewModel,
|
|
||||||
kind = TimelineKind.Day,
|
|
||||||
selectedView = selectedView,
|
|
||||||
onSelectView = onSelectView,
|
|
||||||
// Tapping the one column's header would only reopen this day.
|
|
||||||
onOpenDay = {},
|
|
||||||
onEventClick = onEventClick,
|
|
||||||
onOpenSettings = onOpenSettings,
|
|
||||||
onOpenSearch = onOpenSearch,
|
|
||||||
onCreateEvent = onCreateEvent,
|
|
||||||
quickSwitchViews = quickSwitchViews,
|
|
||||||
drawerViewOrder = drawerViewOrder,
|
|
||||||
todayInToolbar = todayInToolbar,
|
|
||||||
modifier = modifier,
|
|
||||||
)
|
|
||||||
|
|
||||||
/** What sets the timeline views apart; everything else they share. */
|
/** What sets the timeline views apart; everything else they share. */
|
||||||
internal enum class TimelineKind(
|
internal enum class TimelineKind(
|
||||||
@@ -325,12 +293,21 @@ internal enum class TimelineKind(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal val CalendarView.timelineKind: TimelineKind
|
||||||
|
get() = when (this) {
|
||||||
|
CalendarView.MultiDay -> TimelineKind.MultiDay
|
||||||
|
CalendarView.Day -> TimelineKind.Day
|
||||||
|
else -> TimelineKind.Week
|
||||||
|
}
|
||||||
|
|
||||||
|
/** How long a switch waits for the view it goes to before resizing into it without its events. */
|
||||||
|
private const val MORPH_DATA_WAIT_MS = 150L
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun PagedTimelineScreen(
|
private fun PagedTimelineScreen(
|
||||||
viewModel: PagedTimelineViewModel,
|
view: CalendarView,
|
||||||
kind: TimelineKind,
|
viewModels: TimelineViewModels,
|
||||||
selectedView: CalendarView,
|
|
||||||
onSelectView: (CalendarView) -> Unit,
|
onSelectView: (CalendarView) -> Unit,
|
||||||
onOpenDay: (LocalDate) -> Unit,
|
onOpenDay: (LocalDate) -> Unit,
|
||||||
onEventClick: (EventInstance) -> Unit,
|
onEventClick: (EventInstance) -> Unit,
|
||||||
@@ -342,10 +319,23 @@ private fun PagedTimelineScreen(
|
|||||||
todayInToolbar: Boolean,
|
todayInToolbar: Boolean,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
) {
|
) {
|
||||||
|
// The view on screen, which trails [view] while the columns resize into it.
|
||||||
|
var shown by remember { mutableStateOf(view) }
|
||||||
|
var morph by remember { mutableStateOf<TimelineMorph?>(null) }
|
||||||
|
// The strip height a switch ended on, for the pager it hands over to.
|
||||||
|
var handoffStrip by remember { mutableStateOf<Dp?>(null) }
|
||||||
|
val viewModel = viewModels[shown]
|
||||||
|
val kind = shown.timelineKind
|
||||||
|
|
||||||
// Opens on the focused date, then carries it along as the pages move.
|
// Opens on the focused date, then carries it along as the pages move.
|
||||||
val focus = LocalViewFocus.current
|
val focus = LocalViewFocus.current
|
||||||
EnterOnFocus(viewModel) { viewModel.jumpTo(it) }
|
EnterOnFocus(viewModel) { viewModel.jumpTo(it) }
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val loadedState by viewModel.state.collectAsStateWithLifecycle()
|
||||||
|
// A view not shown before has nothing loaded yet; the one before it stands
|
||||||
|
// in, as its pages load their own days anyway, rather than a skeleton.
|
||||||
|
var lastState by remember { mutableStateOf<WeekUiState>(WeekUiState.Loading) }
|
||||||
|
val state = if (loadedState is WeekUiState.Loading) lastState else loadedState
|
||||||
|
SideEffect { if (loadedState !is WeekUiState.Loading) lastState = loadedState }
|
||||||
val anchor by viewModel.anchor.collectAsStateWithLifecycle()
|
val anchor by viewModel.anchor.collectAsStateWithLifecycle()
|
||||||
val rule by viewModel.rule.collectAsStateWithLifecycle()
|
val rule by viewModel.rule.collectAsStateWithLifecycle()
|
||||||
val position = remember(anchor, rule) { positionOf(anchor, rule) }
|
val position = remember(anchor, rule) { positionOf(anchor, rule) }
|
||||||
@@ -364,9 +354,9 @@ private fun PagedTimelineScreen(
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
// A new grid numbers the pages differently, so it gets a pager of its own,
|
// A new grid numbers the pages differently, so it gets a pager of its own,
|
||||||
// opened straight on the anchor's page: the first day of the week changing,
|
// opened straight on the anchor's page: another view, the first day of the
|
||||||
// or a multi-day jump moving another date into the first column.
|
// week changing, or a multi-day jump moving another date into the first column.
|
||||||
val pagerState = key(grid) {
|
val pagerState = key(viewModel, grid) {
|
||||||
rememberPagerState(initialPage = anchorPage) { grid.pageCount }
|
rememberPagerState(initialPage = anchorPage) { grid.pageCount }
|
||||||
}
|
}
|
||||||
val pageSpec = rememberCalendarPageSpec()
|
val pageSpec = rememberCalendarPageSpec()
|
||||||
@@ -387,9 +377,52 @@ private fun PagedTimelineScreen(
|
|||||||
focus?.let { it.date = grid.focusOnPage(it.date, page) }
|
focus?.let { it.date = grid.focusOnPage(it.date, page) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The page under the finger, so the title turns over mid-swipe, not after it.
|
|
||||||
val weekStart = grid.startForPage(pagerState.currentPage)
|
// Switching to another timeline view resizes the columns into it (#184):
|
||||||
val weekEnd = weekStart.plus(grid.dayCount - 1, kotlinx.datetime.DateTimeUnit.DAY)
|
// from the page on screen to the target view's page around the focused
|
||||||
|
// date, then hands over to that view's pager at the page the resize ended on.
|
||||||
|
val currentShown by rememberUpdatedState(shown)
|
||||||
|
val currentPager by rememberUpdatedState(pagerState)
|
||||||
|
val currentGrid by rememberUpdatedState(grid)
|
||||||
|
val currentState by rememberUpdatedState(state)
|
||||||
|
LaunchedEffect(view) {
|
||||||
|
// A switch interrupted by another lands first, and this one starts from there.
|
||||||
|
snapshotFlow { morph }.first { it == null }
|
||||||
|
withFrameNanos { }
|
||||||
|
if (view == currentShown) return@LaunchedEffect
|
||||||
|
val from = currentShown
|
||||||
|
val fromGrid = currentGrid
|
||||||
|
val fromSpan = DaySpan(fromGrid.startForPage(currentPager.currentPage), fromGrid.dayCount)
|
||||||
|
val target = (focus?.date ?: fromSpan.start).coerceIn(fromSpan.start, fromSpan.end)
|
||||||
|
focus?.date = target
|
||||||
|
val toModel = viewModels[view]
|
||||||
|
toModel.jumpTo(target)
|
||||||
|
val to = positionOf(target, toModel.rule.value)
|
||||||
|
val toSpan = DaySpan(to.grid.startForPage(to.page), to.grid.dayCount)
|
||||||
|
val fromLoaded = viewModels[from].span(fromSpan)
|
||||||
|
val toLoaded = toModel.span(toSpan)
|
||||||
|
if (reduceMotion || currentState !is WeekUiState.Success) {
|
||||||
|
shown = view
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
val switch = TimelineMorph(from.timelineKind, view.timelineKind, fromSpan, toSpan, fromLoaded, toLoaded)
|
||||||
|
try {
|
||||||
|
withTimeoutOrNull(MORPH_DATA_WAIT_MS) { toLoaded.first { it !is WeekUiState.Loading } }
|
||||||
|
morph = switch
|
||||||
|
switch.progress.animateTo(1f, pageSpec)
|
||||||
|
} finally {
|
||||||
|
handoffStrip = (toLoaded.value as? WeekUiState.Success)?.allDayStripHeight()
|
||||||
|
shown = view
|
||||||
|
morph = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The page under the finger, so the title turns over mid-swipe, not after
|
||||||
|
// it; mid-switch, the page being switched to.
|
||||||
|
val weekStart = morph?.to?.start ?: grid.startForPage(pagerState.currentPage)
|
||||||
|
val titleKind = morph?.toKind ?: kind
|
||||||
|
val dayCount = morph?.to?.count ?: grid.dayCount
|
||||||
|
val weekEnd = weekStart.plus(dayCount - 1, kotlinx.datetime.DateTimeUnit.DAY)
|
||||||
|
|
||||||
// Off the ticking clock rather than the loaded page, so it turns over at
|
// Off the ticking clock rather than the loaded page, so it turns over at
|
||||||
// midnight; derived, so the per-minute tick doesn't recompose the screen.
|
// midnight; derived, so the per-minute tick doesn't recompose the screen.
|
||||||
@@ -417,12 +450,12 @@ private fun PagedTimelineScreen(
|
|||||||
gesturesEnabled = drawerState.isOpen,
|
gesturesEnabled = drawerState.isOpen,
|
||||||
drawerContent = {
|
drawerContent = {
|
||||||
CalendarDrawer(
|
CalendarDrawer(
|
||||||
currentView = selectedView,
|
currentView = view,
|
||||||
currentDate = weekStart,
|
currentDate = weekStart,
|
||||||
drawerState = drawerState,
|
drawerState = drawerState,
|
||||||
viewOrder = drawerViewOrder,
|
viewOrder = drawerViewOrder,
|
||||||
onSelectView = { view ->
|
onSelectView = { target ->
|
||||||
onSelectView(view)
|
onSelectView(target)
|
||||||
scope.launch { drawerState.close() }
|
scope.launch { drawerState.close() }
|
||||||
},
|
},
|
||||||
onJumpToDate = { target ->
|
onJumpToDate = { target ->
|
||||||
@@ -440,12 +473,12 @@ private fun PagedTimelineScreen(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = {
|
topBar = {
|
||||||
WeekTopBar(
|
WeekTopBar(
|
||||||
kind = kind,
|
kind = titleKind,
|
||||||
weekStart = weekStart,
|
weekStart = weekStart,
|
||||||
weekEnd = weekEnd,
|
weekEnd = weekEnd,
|
||||||
currentYear = today.year,
|
currentYear = today.year,
|
||||||
selectedView = selectedView,
|
selectedView = view,
|
||||||
onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) },
|
onCycleView = { onSelectView(view.next(quickSwitchViews)) },
|
||||||
quickSwitchViews = quickSwitchViews,
|
quickSwitchViews = quickSwitchViews,
|
||||||
onOpenDrawer = { scope.launch { drawerState.open() } },
|
onOpenDrawer = { scope.launch { drawerState.open() } },
|
||||||
onOpenSearch = onOpenSearch,
|
onOpenSearch = onOpenSearch,
|
||||||
@@ -457,10 +490,10 @@ private fun PagedTimelineScreen(
|
|||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
CalendarFabColumn(
|
CalendarFabColumn(
|
||||||
todayVisible = !isOnCurrentWeek && !todayInToolbar,
|
todayVisible = !isOnCurrentWeek && !todayInToolbar,
|
||||||
todayText = stringResource(kind.todayText),
|
todayText = stringResource(titleKind.todayText),
|
||||||
onToday = jumpToToday,
|
onToday = jumpToToday,
|
||||||
onCreate = {
|
onCreate = {
|
||||||
// Anchor on today when it's in view, else the week's first day.
|
// Anchor on today when it's in view, else the page's first day.
|
||||||
onCreateEvent(if (isOnCurrentWeek) today else weekStart, null)
|
onCreateEvent(if (isOnCurrentWeek) today else weekStart, null)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -472,6 +505,8 @@ private fun PagedTimelineScreen(
|
|||||||
pagerState = pagerState,
|
pagerState = pagerState,
|
||||||
grid = grid,
|
grid = grid,
|
||||||
kind = kind,
|
kind = kind,
|
||||||
|
morph = morph,
|
||||||
|
handoffStrip = handoffStrip,
|
||||||
span = viewModel::span,
|
span = viewModel::span,
|
||||||
onRetry = viewModel::goToToday,
|
onRetry = viewModel::goToToday,
|
||||||
onEventClick = onEventClick,
|
onEventClick = onEventClick,
|
||||||
@@ -492,6 +527,8 @@ private fun WeekContent(
|
|||||||
pagerState: PagerState,
|
pagerState: PagerState,
|
||||||
grid: PageGrid,
|
grid: PageGrid,
|
||||||
kind: TimelineKind,
|
kind: TimelineKind,
|
||||||
|
morph: TimelineMorph?,
|
||||||
|
handoffStrip: Dp?,
|
||||||
span: (DaySpan) -> StateFlow<WeekUiState>,
|
span: (DaySpan) -> StateFlow<WeekUiState>,
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit,
|
||||||
onEventClick: (EventInstance) -> Unit,
|
onEventClick: (EventInstance) -> Unit,
|
||||||
@@ -536,13 +573,24 @@ private fun WeekContent(
|
|||||||
when (s) {
|
when (s) {
|
||||||
WeekUiState.Loading -> WeekLoading(dayCount = grid.dayCount)
|
WeekUiState.Loading -> WeekLoading(dayCount = grid.dayCount)
|
||||||
is WeekUiState.Failure -> CalendarFailure(reason = s.reason, onRetry = onRetry)
|
is WeekUiState.Failure -> CalendarFailure(reason = s.reason, onRetry = onRetry)
|
||||||
is WeekUiState.Success -> WeekPager(
|
is WeekUiState.Success -> if (morph != null) {
|
||||||
|
TimelineMorphFrame(
|
||||||
|
morph = morph,
|
||||||
|
today = s.today,
|
||||||
|
scrollState = scrollState,
|
||||||
|
dragController = dragController,
|
||||||
|
onEventClick = onEventClick,
|
||||||
|
onOpenDay = onOpenDay,
|
||||||
|
onCreateAt = onCreateAt,
|
||||||
|
onDrop = onDrop,
|
||||||
|
)
|
||||||
|
} else WeekPager(
|
||||||
pagerState = pagerState,
|
pagerState = pagerState,
|
||||||
grid = grid,
|
grid = grid,
|
||||||
kind = kind,
|
kind = kind,
|
||||||
span = span,
|
span = span,
|
||||||
today = s.today,
|
today = s.today,
|
||||||
initialStripHeight = s.allDayStripHeight(),
|
initialStripHeight = handoffStrip ?: s.allDayStripHeight(),
|
||||||
scrollState = scrollState,
|
scrollState = scrollState,
|
||||||
dragController = dragController,
|
dragController = dragController,
|
||||||
onEventClick = onEventClick,
|
onEventClick = onEventClick,
|
||||||
@@ -637,6 +685,70 @@ private fun emptySpan(span: DaySpan, today: LocalDate): WeekUiState.Success {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timeline mid-switch between two views (#184): every day of both pages,
|
||||||
|
* each column between its place on the one and on the other, with no pager
|
||||||
|
* under them. It starts and ends exactly as the two pages lay out, so the
|
||||||
|
* pagers either side hand over without a jump.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun TimelineMorphFrame(
|
||||||
|
morph: TimelineMorph,
|
||||||
|
today: LocalDate,
|
||||||
|
scrollState: ScrollState,
|
||||||
|
dragController: TimelineDragController,
|
||||||
|
onEventClick: (EventInstance) -> Unit,
|
||||||
|
onOpenDay: (LocalDate) -> Unit,
|
||||||
|
onCreateAt: (LocalDate, Int) -> Unit,
|
||||||
|
onDrop: (TimelineDrop) -> Unit,
|
||||||
|
) {
|
||||||
|
val fromState by morph.fromState.collectAsStateWithLifecycle()
|
||||||
|
val toState by morph.toState.collectAsStateWithLifecycle()
|
||||||
|
val from = fromState as? WeekUiState.Success
|
||||||
|
val to = toState as? WeekUiState.Success
|
||||||
|
val geometry = remember(morph) { ColumnGeometry(morph.from, morph.to) { morph.progress.value } }
|
||||||
|
val bars = remember(from, to) { morphBars(from, to) }
|
||||||
|
val timedFor: (LocalDate) -> List<TimedBlock> = remember(morph, from, to) {
|
||||||
|
{ date ->
|
||||||
|
val side = if (morph.sourceOf(date) == TimelineMorph.Side.From) from else to
|
||||||
|
side?.timedByDay?.get(date).orEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val progress = morph.progress.value
|
||||||
|
val fromStrip = from?.allDayStripHeight() ?: 0.dp
|
||||||
|
val toStrip = to?.allDayStripHeight() ?: fromStrip
|
||||||
|
val allDayHeight = androidx.compose.ui.unit.lerp(fromStrip, toStrip, progress.coerceIn(0f, 1f))
|
||||||
|
val weekNumbers = listOf(morph.fromKind, morph.toKind).map { if (it.showWeekNumber) 1f else 0f }
|
||||||
|
val weekNumberAlpha = lerp(weekNumbers[0], weekNumbers[1], progress).coerceIn(0f, 1f)
|
||||||
|
// The events travel inside the columns here, not between views.
|
||||||
|
CompositionLocalProvider(LocalViewMorph provides null) {
|
||||||
|
WeekFrame(
|
||||||
|
weekStart = if (morph.toKind.showWeekNumber) morph.to.start else morph.from.start,
|
||||||
|
showWeekNumber = weekNumberAlpha > 0f,
|
||||||
|
allDayHeight = allDayHeight,
|
||||||
|
weekNumberAlpha = weekNumberAlpha,
|
||||||
|
scrollState = scrollState,
|
||||||
|
dragController = dragController,
|
||||||
|
) {
|
||||||
|
TimelinePageContent(
|
||||||
|
geometry = geometry,
|
||||||
|
today = today,
|
||||||
|
timedFor = timedFor,
|
||||||
|
bars = bars,
|
||||||
|
headerBias = lerp(morph.fromKind.headerBias, morph.toKind.headerBias, progress.coerceIn(0f, 1f)),
|
||||||
|
allDayHeight = allDayHeight,
|
||||||
|
scrollState = scrollState,
|
||||||
|
dragController = dragController,
|
||||||
|
active = false,
|
||||||
|
onEventClick = onEventClick,
|
||||||
|
onOpenDay = onOpenDay,
|
||||||
|
onCreateAt = onCreateAt,
|
||||||
|
onDrop = onDrop,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* What stays put while the weeks swipe: the week-number badge and the hour
|
* What stays put while the weeks swipe: the week-number badge and the hour
|
||||||
* gutter, with [pages] filling the rest.
|
* gutter, with [pages] filling the rest.
|
||||||
@@ -646,6 +758,7 @@ private fun WeekFrame(
|
|||||||
weekStart: LocalDate,
|
weekStart: LocalDate,
|
||||||
showWeekNumber: Boolean,
|
showWeekNumber: Boolean,
|
||||||
allDayHeight: Dp,
|
allDayHeight: Dp,
|
||||||
|
weekNumberAlpha: Float = 1f,
|
||||||
scrollState: ScrollState,
|
scrollState: ScrollState,
|
||||||
dragController: TimelineDragController,
|
dragController: TimelineDragController,
|
||||||
pages: @Composable () -> Unit,
|
pages: @Composable () -> Unit,
|
||||||
@@ -657,7 +770,11 @@ private fun WeekFrame(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(MaterialTheme.colorScheme.surface),
|
.background(MaterialTheme.colorScheme.surface),
|
||||||
) {
|
) {
|
||||||
WeekNumberCell(weekStart, showBadge = showWeekNumber)
|
WeekNumberCell(
|
||||||
|
weekStart = weekStart,
|
||||||
|
showBadge = showWeekNumber,
|
||||||
|
modifier = if (weekNumberAlpha < 1f) Modifier.alpha(weekNumberAlpha) else Modifier,
|
||||||
|
)
|
||||||
Spacer(Modifier.height(allDayHeight))
|
Spacer(Modifier.height(allDayHeight))
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(TIMELINE_TOP_GAP))
|
Spacer(Modifier.height(TIMELINE_TOP_GAP))
|
||||||
@@ -942,10 +1059,10 @@ private fun WeekDayHeader(
|
|||||||
* bar's hamburger (see GUTTER_CONTENT_START_INSET).
|
* bar's hamburger (see GUTTER_CONTENT_START_INSET).
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun WeekNumberCell(weekStart: LocalDate, showBadge: Boolean) {
|
private fun WeekNumberCell(weekStart: LocalDate, showBadge: Boolean, modifier: Modifier = Modifier) {
|
||||||
val weekNumber = remember(weekStart) { weekStart.toJavaLocalDate().isoWeekNumber() }
|
val weekNumber = remember(weekStart) { weekStart.toJavaLocalDate().isoWeekNumber() }
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(
|
.padding(
|
||||||
start = GUTTER_CONTENT_START_INSET,
|
start = GUTTER_CONTENT_START_INSET,
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.ui.common
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
/** Which screen draws each view: the timeline views share one, so switching among them resizes it (#184). */
|
||||||
|
class ViewScreenTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `the day, multi-day and week views share the timeline`() {
|
||||||
|
assertThat(listOf(CalendarView.Day, CalendarView.MultiDay, CalendarView.Week).map { it.screen }.toSet())
|
||||||
|
.containsExactly(ViewScreen.Timeline)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `month and agenda each have their own screen`() {
|
||||||
|
assertThat(CalendarView.Month.screen).isEqualTo(ViewScreen.Month)
|
||||||
|
assertThat(CalendarView.Agenda.screen).isEqualTo(ViewScreen.Agenda)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user