Share one focused date across views (#184)
This commit is contained in:
@@ -40,6 +40,9 @@ import de.jeanlucmakiola.calendula.ui.common.EventMoveScope
|
||||
import de.jeanlucmakiola.calendula.ui.common.LocalEventMove
|
||||
import de.jeanlucmakiola.calendula.ui.common.LocalManageCalendars
|
||||
import de.jeanlucmakiola.calendula.ui.common.LocalTimelineScroll
|
||||
import de.jeanlucmakiola.calendula.ui.common.LocalViewFocus
|
||||
import de.jeanlucmakiola.calendula.ui.common.ViewFocus
|
||||
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
|
||||
@@ -120,10 +123,11 @@ fun CalendarHost(
|
||||
val view = viewStack.last()
|
||||
val onSelectView: (CalendarView) -> Unit = { viewStack = viewStack.selectView(it) }
|
||||
|
||||
// Tapping a day in the month grid opens the day view anchored to that date.
|
||||
var pendingDayIso by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
// The date every view opens on and carries along (#184); tapping a day
|
||||
// anywhere focuses it and opens the day view there.
|
||||
val focus = rememberSaveable(saver = ViewFocusSaver) { ViewFocus(localToday()) }
|
||||
val onOpenDay: (LocalDate) -> Unit = { date ->
|
||||
pendingDayIso = date.toString()
|
||||
focus.date = date
|
||||
viewStack = viewStack.drillToDay()
|
||||
}
|
||||
|
||||
@@ -280,7 +284,7 @@ fun CalendarHost(
|
||||
// that would cover it, so the open doesn't land under Settings/form.
|
||||
dismissCoveringOverlays()
|
||||
createDateIso = null
|
||||
pendingDayIso = req.dateIso
|
||||
focus.date = LocalDate.parse(req.dateIso)
|
||||
// No widget source (an external date tap) roots over the default
|
||||
// home view, so backing out of the day returns home then exits.
|
||||
viewStack = viewBaseStack(defaultView, req.source ?: defaultView).drillToDay()
|
||||
@@ -304,7 +308,7 @@ fun CalendarHost(
|
||||
// default home — so backing out returns to the default, then exits.
|
||||
dismissCoveringOverlays()
|
||||
createDateIso = null
|
||||
pendingDayIso = null
|
||||
focus.date = localToday()
|
||||
viewStack = viewBaseStack(defaultView, req.view ?: defaultView)
|
||||
onWidgetNavConsumed()
|
||||
}
|
||||
@@ -386,6 +390,7 @@ fun CalendarHost(
|
||||
CompositionLocalProvider(
|
||||
LocalEventMove provides moveScope,
|
||||
LocalTimelineScroll provides timelineScroll,
|
||||
LocalViewFocus provides focus,
|
||||
// The failure state's way out when every calendar is switched off
|
||||
// (#239); nothing else in the calendar surfaces navigates here.
|
||||
LocalManageCalendars provides { showCalendars = true },
|
||||
@@ -438,7 +443,6 @@ fun CalendarHost(
|
||||
onOpenSettings = onOpenSettings,
|
||||
onOpenSearch = onOpenSearch,
|
||||
onCreateEvent = onCreateEvent,
|
||||
initialDateIso = pendingDayIso,
|
||||
quickSwitchViews = quickSwitchViews,
|
||||
drawerViewOrder = drawerViewOrder,
|
||||
todayInToolbar = todayInToolbar,
|
||||
@@ -674,3 +678,6 @@ private val viewStackSaver = listSaver<List<CalendarView>, Int>(
|
||||
save = { stack -> stack.map(CalendarView::ordinal) },
|
||||
restore = { ordinals -> ordinals.map { CalendarView.entries[it] } },
|
||||
)
|
||||
|
||||
private fun localToday(): LocalDate =
|
||||
Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package de.jeanlucmakiola.calendula.ui.agenda
|
||||
|
||||
import de.jeanlucmakiola.calendula.ui.common.LocalViewFocus
|
||||
import de.jeanlucmakiola.calendula.ui.common.EnterOnFocus
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import de.jeanlucmakiola.calendula.ui.common.ViewMorphKey
|
||||
import de.jeanlucmakiola.calendula.ui.common.morphEvent
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
@@ -96,8 +101,15 @@ fun AgendaScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: AgendaViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
// Opens on the focused date and hands back the day scrolled to the top.
|
||||
val focus = LocalViewFocus.current
|
||||
EnterOnFocus(viewModel) { if (it != viewModel.anchor.value) viewModel.goToDate(it) }
|
||||
val loadedState by viewModel.state.collectAsStateWithLifecycle()
|
||||
val anchor by viewModel.anchor.collectAsStateWithLifecycle()
|
||||
// Rows from before the anchor moved belong to another window: they would
|
||||
// show the wrong days, and the view switch would match the wrong events.
|
||||
val state = loadedState.takeUnless { it is AgendaUiState.Success && it.anchor != anchor }
|
||||
?: AgendaUiState.Loading
|
||||
val pastDisplay by viewModel.pastEventDisplay.collectAsStateWithLifecycle()
|
||||
val showToday by viewModel.showToday.collectAsStateWithLifecycle()
|
||||
val weekStart by viewModel.weekStart.collectAsStateWithLifecycle()
|
||||
@@ -126,11 +138,12 @@ fun AgendaScreen(
|
||||
// requested: scrolling before the new rows compose leaves the list keyed to
|
||||
// a row that reappears further down, and it follows it there.
|
||||
LaunchedEffect(successState?.anchor) { listState.scrollToItem(0) }
|
||||
val jumpToToday = {
|
||||
val jumpToToday: () -> Unit = {
|
||||
// Off today, moving the anchor is what resets the list (above); on today
|
||||
// the window doesn't change, so the scroll back is the whole action.
|
||||
if (isOnToday) scope.launch { listState.animateScrollToItem(0) }
|
||||
viewModel.goToToday()
|
||||
(state as? AgendaUiState.Success)?.today?.let { focus?.date = it }
|
||||
}
|
||||
|
||||
ModalNavigationDrawer(
|
||||
@@ -146,6 +159,7 @@ fun AgendaScreen(
|
||||
scope.launch { drawerState.close() }
|
||||
},
|
||||
onJumpToDate = { target ->
|
||||
focus?.date = target
|
||||
viewModel.goToDate(target)
|
||||
scope.launch { drawerState.close() }
|
||||
},
|
||||
@@ -227,6 +241,7 @@ fun AgendaScreen(
|
||||
onRetry = viewModel::goToToday,
|
||||
onEventClick = onEventClick,
|
||||
onOpenDay = onOpenDay,
|
||||
onTopDay = { focus?.date = it },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth(),
|
||||
@@ -328,6 +343,7 @@ internal fun AgendaContent(
|
||||
onOpenDay: (LocalDate) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
listState: LazyListState = rememberLazyListState(),
|
||||
onTopDay: ((LocalDate) -> Unit)? = null,
|
||||
) {
|
||||
when (state) {
|
||||
AgendaUiState.Loading -> Box(modifier)
|
||||
@@ -370,6 +386,7 @@ internal fun AgendaContent(
|
||||
now = now,
|
||||
onEventClick = onEventClick,
|
||||
onOpenDay = onOpenDay,
|
||||
onTopDay = onTopDay,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
@@ -388,8 +405,20 @@ private fun AgendaList(
|
||||
now: Instant,
|
||||
onEventClick: (EventInstance) -> Unit,
|
||||
onOpenDay: (LocalDate) -> Unit,
|
||||
onTopDay: ((LocalDate) -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (onTopDay != null) {
|
||||
val itemDates by rememberUpdatedState(agendaItemDates(days))
|
||||
val currentOnTopDay by rememberUpdatedState(onTopDay)
|
||||
// The day the list opened on is where the view already is; only a
|
||||
// scroll moves it on.
|
||||
LaunchedEffect(listState) {
|
||||
snapshotFlow { listState.firstVisibleItemIndex }
|
||||
.drop(1)
|
||||
.collect { index -> itemDates.getOrNull(index)?.let(currentOnTopDay) }
|
||||
}
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
state = listState,
|
||||
@@ -432,6 +461,13 @@ private fun AgendaList(
|
||||
}
|
||||
}
|
||||
|
||||
/** The day of each item [AgendaList] lays out for [days], in its order. */
|
||||
internal fun agendaItemDates(days: List<AgendaDay>): List<LocalDate> =
|
||||
days.flatMap { day ->
|
||||
// Header, the placeholder or the events, then the gap.
|
||||
List(2 + day.events.size.coerceAtLeast(1)) { day.date }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AgendaEmpty(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package de.jeanlucmakiola.calendula.ui.common
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
/**
|
||||
* The date every calendar view is looking at (#184), so switching views keeps
|
||||
* the same days on screen and what is shown in both can travel between them.
|
||||
*
|
||||
* A view reads it once, as it comes on screen, and writes it back only when the
|
||||
* user moves: a view settling where it opened must not nudge it, or switching
|
||||
* back and forth would drift.
|
||||
*/
|
||||
@Stable
|
||||
class ViewFocus(initial: LocalDate) {
|
||||
var date by mutableStateOf(initial)
|
||||
}
|
||||
|
||||
val ViewFocusSaver: Saver<ViewFocus, String> = Saver(
|
||||
save = { it.date.toString() },
|
||||
restore = { ViewFocus(LocalDate.parse(it)) },
|
||||
)
|
||||
|
||||
val LocalViewFocus = staticCompositionLocalOf<ViewFocus?> { null }
|
||||
|
||||
/**
|
||||
* Runs [enter] with the focused date once, as the view comes on screen, before
|
||||
* it reads its own position: its first frame is then already on that date.
|
||||
*/
|
||||
@Composable
|
||||
fun <T : Any> EnterOnFocus(key: T, enter: (LocalDate) -> Unit) {
|
||||
val focus = LocalViewFocus.current
|
||||
remember(key) { focus?.let { enter(it.date) } }
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import de.jeanlucmakiola.calendula.ui.common.LocalViewMorph
|
||||
import de.jeanlucmakiola.calendula.ui.common.EnterOnFocus
|
||||
import de.jeanlucmakiola.calendula.ui.common.LocalViewFocus
|
||||
import de.jeanlucmakiola.calendula.ui.common.ViewMorphKey
|
||||
import de.jeanlucmakiola.calendula.ui.common.morphDay
|
||||
import de.jeanlucmakiola.calendula.ui.common.morphEvent
|
||||
@@ -215,6 +217,9 @@ fun MonthScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: MonthViewModel = hiltViewModel(),
|
||||
) {
|
||||
// Opens on the focused date's month, and hands its selection back as it moves.
|
||||
val focus = LocalViewFocus.current
|
||||
EnterOnFocus(viewModel) { viewModel.goToDate(it) }
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val continuousState by viewModel.continuousState.collectAsStateWithLifecycle()
|
||||
val month by viewModel.month.collectAsStateWithLifecycle()
|
||||
@@ -239,7 +244,7 @@ fun MonthScreen(
|
||||
// The paged and split styles' months. Made afresh whenever a paged style
|
||||
// comes on screen, opening on the month the scrolling style was showing: a
|
||||
// pager that was never laid out can't be scrolled to follow it meanwhile.
|
||||
val anchorPage by viewModel.anchorPage.collectAsStateWithLifecycle()
|
||||
val anchorPage = monthIndexOf(month)
|
||||
val pagerState = key(scrolling) {
|
||||
rememberPagerState(initialPage = anchorPage) { continuousMonthCount() }
|
||||
}
|
||||
@@ -256,6 +261,11 @@ fun MonthScreen(
|
||||
LaunchedEffect(pagerState) {
|
||||
snapshotFlow { pagerState.settledPage }.drop(1).collect(viewModel::onPageSettled)
|
||||
}
|
||||
// The selection follows every move — paging, scrolling, today, a jump — so
|
||||
// it is what the other views open on. Not the one this view opened with.
|
||||
LaunchedEffect(viewModel) {
|
||||
snapshotFlow { selectedDate }.drop(1).collect { focus?.date = it }
|
||||
}
|
||||
val dense = viewStyle == MonthViewStyle.Dense
|
||||
|
||||
// Today, from whichever state is driving; the clock only covers the first
|
||||
|
||||
@@ -94,15 +94,6 @@ class MonthViewModel @Inject constructor(
|
||||
private val _month = MutableStateFlow(YearMonth(todayDate.year, todayDate.month))
|
||||
val month: StateFlow<YearMonth> = _month
|
||||
|
||||
/** The pager page the anchor month sits on, for the paged and split styles. */
|
||||
val anchorPage: StateFlow<Int> = _month
|
||||
.map { monthIndexOf(it) }
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000L),
|
||||
initialValue = monthIndexOf(_month.value),
|
||||
)
|
||||
|
||||
/**
|
||||
* The anchor month's state. Once loaded it never falls back to
|
||||
* [MonthUiState.Loading]: moving the anchor keeps the last result until the
|
||||
|
||||
@@ -93,3 +93,15 @@ data object DayGrid : PageGrid {
|
||||
override fun pageFor(date: LocalDate): Int =
|
||||
(DAY_EPOCH_PAGE + DAY_EPOCH.daysUntil(date)).coerceIn(0, DAY_PAGE_COUNT - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the focused date goes when the pager settles on [page] (#184): kept if
|
||||
* the page still shows it, otherwise moved by the pages travelled, so it keeps
|
||||
* its place in the page — the same weekday a week on.
|
||||
*/
|
||||
internal fun PageGrid.focusOnPage(focus: LocalDate, page: Int): LocalDate {
|
||||
val start = startForPage(page)
|
||||
val end = start.plus(dayCount - 1, DateTimeUnit.DAY)
|
||||
if (focus in start..end) return focus
|
||||
return focus.plus((page - pageFor(focus)) * dayCount, DateTimeUnit.DAY).coerceIn(start, end)
|
||||
}
|
||||
|
||||
@@ -135,6 +135,8 @@ import de.jeanlucmakiola.calendula.ui.common.trimmedLines
|
||||
import de.jeanlucmakiola.calendula.ui.common.NowLine
|
||||
import de.jeanlucmakiola.calendula.ui.common.rememberCurrentMinute
|
||||
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
||||
import de.jeanlucmakiola.calendula.ui.common.EnterOnFocus
|
||||
import de.jeanlucmakiola.calendula.ui.common.LocalViewFocus
|
||||
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarFadeSpec
|
||||
import de.jeanlucmakiola.calendula.ui.common.TimelineGrounds
|
||||
import de.jeanlucmakiola.calendula.ui.common.ViewMorphKey
|
||||
@@ -278,7 +280,6 @@ fun DayScreen(
|
||||
drawerViewOrder: List<CalendarView> = IMPLEMENTED_VIEWS,
|
||||
todayInToolbar: Boolean = false,
|
||||
modifier: Modifier = Modifier,
|
||||
initialDateIso: String? = null,
|
||||
viewModel: DayViewModel = hiltViewModel(),
|
||||
) = PagedTimelineScreen(
|
||||
viewModel = viewModel,
|
||||
@@ -295,7 +296,6 @@ fun DayScreen(
|
||||
drawerViewOrder = drawerViewOrder,
|
||||
todayInToolbar = todayInToolbar,
|
||||
modifier = modifier,
|
||||
initialDate = initialDateIso?.let(LocalDate::parse),
|
||||
)
|
||||
|
||||
/** What sets the timeline views apart; everything else they share. */
|
||||
@@ -336,11 +336,14 @@ private fun PagedTimelineScreen(
|
||||
drawerViewOrder: List<CalendarView>,
|
||||
todayInToolbar: Boolean,
|
||||
modifier: Modifier,
|
||||
initialDate: LocalDate? = null,
|
||||
) {
|
||||
LaunchedEffect(initialDate) { initialDate?.let(viewModel::jumpTo) }
|
||||
// Opens on the focused date, then carries it along as the pages move.
|
||||
val focus = LocalViewFocus.current
|
||||
EnterOnFocus(viewModel) { viewModel.jumpTo(it) }
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val position by viewModel.position.collectAsStateWithLifecycle()
|
||||
val anchor by viewModel.anchor.collectAsStateWithLifecycle()
|
||||
val rule by viewModel.rule.collectAsStateWithLifecycle()
|
||||
val position = remember(anchor, rule) { positionOf(anchor, rule) }
|
||||
val grid = position.grid
|
||||
val anchorPage = position.page
|
||||
val dimCompleted by viewModel.dimCompletedEvents.collectAsStateWithLifecycle()
|
||||
@@ -374,7 +377,10 @@ private fun PagedTimelineScreen(
|
||||
// The page it opened on is skipped: a grid change landing in between would
|
||||
// read it as a different page.
|
||||
LaunchedEffect(pagerState) {
|
||||
snapshotFlow { pagerState.settledPage }.drop(1).collect(viewModel::onPageSettled)
|
||||
snapshotFlow { pagerState.settledPage }.drop(1).collect { page ->
|
||||
viewModel.onPageSettled(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)
|
||||
@@ -390,6 +396,7 @@ private fun PagedTimelineScreen(
|
||||
// hasn't caught up with the fling yet to tell the difference. A date that
|
||||
// would open mid-page on this grid re-frames the pages around it instead.
|
||||
val jumpToDate: (LocalDate) -> Unit = { target ->
|
||||
focus?.date = target
|
||||
val page = grid.pageFor(target)
|
||||
if (grid is DaysGrid && grid.startForPage(page) != target) {
|
||||
viewModel.jumpTo(target)
|
||||
|
||||
@@ -56,6 +56,10 @@ data class DaySpan(val start: LocalDate, val count: Int)
|
||||
/** How the pager cuts the calendar, and the page the anchor sits on. */
|
||||
data class PagerPosition(val grid: PageGrid, val page: Int)
|
||||
|
||||
/** Where [anchor] sits under [rule]. */
|
||||
fun positionOf(anchor: LocalDate, rule: (LocalDate) -> PageGrid): PagerPosition =
|
||||
rule(anchor).let { PagerPosition(it, it.pageFor(anchor)) }
|
||||
|
||||
/**
|
||||
* A timeline paged in runs of days: the week view, and the multi-day view (#193).
|
||||
* The pager leads; the anchor follows it once it settles, and the grid it is
|
||||
@@ -90,14 +94,22 @@ abstract class PagedTimelineViewModel(
|
||||
// A day on the visible page; the page itself comes from the grid, so a
|
||||
// settings change re-frames the same days instead of jumping.
|
||||
private val _anchor = MutableStateFlow(todayDate)
|
||||
val anchor: StateFlow<LocalDate> = _anchor
|
||||
|
||||
val position: StateFlow<PagerPosition> =
|
||||
combine(_anchor, gridRule) { anchor, rule -> rule(anchor).let { PagerPosition(it, it.pageFor(anchor)) } }
|
||||
/**
|
||||
* The grid for a given anchor, held eagerly so a screen can place its pager
|
||||
* on its very first frame rather than a frame after the anchor moved.
|
||||
*/
|
||||
val rule: StateFlow<(LocalDate) -> PageGrid> =
|
||||
gridRule.stateIn(viewModelScope, SharingStarted.Eagerly, initialRule)
|
||||
|
||||
private val position: StateFlow<PagerPosition> =
|
||||
combine(_anchor, rule) { anchor, rule -> positionOf(anchor, rule) }
|
||||
.distinctUntilChanged()
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000L),
|
||||
initialValue = initialRule(todayDate).let { PagerPosition(it, it.pageFor(todayDate)) },
|
||||
initialValue = positionOf(todayDate, initialRule),
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -141,7 +153,7 @@ abstract class PagedTimelineViewModel(
|
||||
|
||||
/** The pager came to rest on [page]; follow it unless it is already the anchor's. */
|
||||
fun onPageSettled(page: Int) {
|
||||
val grid = position.value.grid
|
||||
val grid = rule.value(_anchor.value)
|
||||
if (grid.pageFor(_anchor.value) != page) _anchor.value = grid.startForPage(page)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package de.jeanlucmakiola.calendula.ui.agenda
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlin.time.Instant
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/** The day each agenda list item belongs to, for the focus it hands back (#184). */
|
||||
class AgendaItemDatesTest {
|
||||
|
||||
private val monday = LocalDate(2026, 9, 28)
|
||||
private val tuesday = LocalDate(2026, 9, 29)
|
||||
|
||||
private fun event(id: Long) = EventInstance(
|
||||
instanceId = id,
|
||||
eventId = id,
|
||||
calendarId = 1L,
|
||||
title = "Event",
|
||||
start = Instant.fromEpochMilliseconds(0),
|
||||
end = Instant.fromEpochMilliseconds(0),
|
||||
isAllDay = false,
|
||||
color = 0xFF000000.toInt(),
|
||||
location = null,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `each day counts its header, events and gap`() {
|
||||
val days = listOf(AgendaDay(monday, listOf(event(1), event(2))), AgendaDay(tuesday, listOf(event(3))))
|
||||
assertThat(agendaItemDates(days)).containsExactly(
|
||||
monday, monday, monday, monday,
|
||||
tuesday, tuesday, tuesday,
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an empty day counts its placeholder`() {
|
||||
assertThat(agendaItemDates(listOf(AgendaDay(monday, emptyList())))).hasSize(3)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package de.jeanlucmakiola.calendula.ui.week
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.datetime.DayOfWeek
|
||||
import kotlinx.datetime.LocalDate
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/** Where the focused date goes as a timeline pages (#184). */
|
||||
class FocusOnPageTest {
|
||||
|
||||
// 2026-09-30 is a Wednesday.
|
||||
private val wednesday = LocalDate(2026, 9, 30)
|
||||
|
||||
@Test
|
||||
fun `a page that still shows the focus keeps it`() {
|
||||
val grid = WeekGrid(DayOfWeek.MONDAY)
|
||||
assertThat(grid.focusOnPage(wednesday, grid.pageFor(wednesday))).isEqualTo(wednesday)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a week on keeps the weekday`() {
|
||||
val grid = WeekGrid(DayOfWeek.MONDAY)
|
||||
val page = grid.pageFor(wednesday)
|
||||
assertThat(grid.focusOnPage(wednesday, page + 1)).isEqualTo(LocalDate(2026, 10, 7))
|
||||
assertThat(grid.focusOnPage(wednesday, page - 2)).isEqualTo(LocalDate(2026, 9, 16))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a run of days keeps the focus's column`() {
|
||||
val grid = DaysGrid.startingAt(LocalDate(2026, 9, 29), 3)
|
||||
val page = grid.pageFor(wednesday)
|
||||
assertThat(grid.focusOnPage(wednesday, page + 1)).isEqualTo(LocalDate(2026, 10, 3))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a day page is the focus`() {
|
||||
val page = DayGrid.pageFor(wednesday)
|
||||
assertThat(DayGrid.focusOnPage(wednesday, page + 5)).isEqualTo(LocalDate(2026, 10, 5))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user