@@ -101,6 +101,7 @@ import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.next
|
import de.jeanlucmakiola.calendula.ui.common.next
|
||||||
import de.jeanlucmakiola.floret.time.isoWeekNumber
|
import de.jeanlucmakiola.floret.time.isoWeekNumber
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.datetime.DayOfWeek
|
import kotlinx.datetime.DayOfWeek
|
||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
@@ -176,10 +177,18 @@ fun MonthScreen(
|
|||||||
LaunchedEffect(listState, continuous) {
|
LaunchedEffect(listState, continuous) {
|
||||||
if (!continuous) return@LaunchedEffect
|
if (!continuous) return@LaunchedEffect
|
||||||
snapshotFlow {
|
snapshotFlow {
|
||||||
|
// Empty before the first layout pass — and reporting a default of 0
|
||||||
|
// for it would claim January 1900 is on screen and drag the whole
|
||||||
|
// window back there.
|
||||||
val visible = listState.layoutInfo.visibleItemsInfo
|
val visible = listState.layoutInfo.visibleItemsInfo
|
||||||
monthIndexForItem(visible.firstOrNull()?.index ?: 0) to
|
if (visible.isEmpty()) {
|
||||||
monthIndexForItem(visible.lastOrNull()?.index ?: 0)
|
null
|
||||||
|
} else {
|
||||||
|
monthIndexForItem(visible.first().index) to
|
||||||
|
monthIndexForItem(visible.last().index)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.filterNotNull()
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.collect { (first, last) -> viewModel.onVisibleMonthsChanged(first, last) }
|
.collect { (first, last) -> viewModel.onVisibleMonthsChanged(first, last) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,7 +148,8 @@ class MonthViewModel @Inject constructor(
|
|||||||
* close enough to a loaded edge to warrant a wider query.
|
* close enough to a loaded edge to warrant a wider query.
|
||||||
*/
|
*/
|
||||||
fun onVisibleMonthsChanged(firstIndex: Int, lastIndex: Int) {
|
fun onVisibleMonthsChanged(firstIndex: Int, lastIndex: Int) {
|
||||||
nextLoadWindow(_loadedMonths.value, firstIndex, lastIndex)?.let { _loadedMonths.value = it }
|
nextLoadWindow(_loadedMonths.value, firstIndex, lastIndex)
|
||||||
|
?.let { _loadedMonths.value = clampMonthWindow(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildContinuousState(
|
private fun buildContinuousState(
|
||||||
@@ -402,6 +403,16 @@ internal fun itemIndexForMonth(monthIndex: Int): Int = monthIndex * 2
|
|||||||
|
|
||||||
internal fun monthIndexForItem(itemIndex: Int): Int = itemIndex / 2
|
internal fun monthIndexForItem(itemIndex: Int): Int = itemIndex / 2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hold a load window inside the months that actually exist. The pad
|
||||||
|
* [nextLoadWindow] adds can otherwise push an index past either end of the
|
||||||
|
* 1900–2100 span, and [yearMonthForIndex] has no month to hand back for one.
|
||||||
|
*/
|
||||||
|
internal fun clampMonthWindow(window: IntRange): IntRange {
|
||||||
|
val last = continuousMonthCount() - 1
|
||||||
|
return window.first.coerceIn(0, last)..window.last.coerceIn(0, last)
|
||||||
|
}
|
||||||
|
|
||||||
internal fun firstOfMonth(ym: YearMonth): LocalDate = LocalDate(ym.year, ym.month, 1)
|
internal fun firstOfMonth(ym: YearMonth): LocalDate = LocalDate(ym.year, ym.month, 1)
|
||||||
|
|
||||||
/** How many week rows [ym] needs under [weekStart] — 4 (a non-leap February), 5 or 6. */
|
/** How many week rows [ym] needs under [weekStart] — 4 (a non-leap February), 5 or 6. */
|
||||||
|
|||||||
@@ -88,6 +88,25 @@ class ContinuousMonthIndexTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a window is clamped to the months that exist`() {
|
||||||
|
// The regression this guards: an empty visible-items list reported as
|
||||||
|
// "month 0", whose padded window ran to -4 — and yearMonthForIndex has
|
||||||
|
// no month for that, so the whole grid failed with "couldn't read".
|
||||||
|
assertThat(clampMonthWindow(-4..4)).isEqualTo(0..4)
|
||||||
|
val last = continuousMonthCount() - 1
|
||||||
|
assertThat(clampMonthWindow((last - 2)..(last + 4))).isEqualTo((last - 2)..last)
|
||||||
|
assertThat(clampMonthWindow(10..20)).isEqualTo(10..20)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `every index a clamped window can hold resolves to a month`() {
|
||||||
|
listOf(clampMonthWindow(-4..4), clampMonthWindow((continuousMonthCount() - 1)..(continuousMonthCount() + 3)))
|
||||||
|
.forEach { window ->
|
||||||
|
window.forEach { index -> yearMonthForIndex(index) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `firstOfMonth is the 1st`() {
|
fun `firstOfMonth is the 1st`() {
|
||||||
assertThat(firstOfMonth(jun26)).isEqualTo(LocalDate(2026, 6, 1))
|
assertThat(firstOfMonth(jun26)).isEqualTo(LocalDate(2026, 6, 1))
|
||||||
|
|||||||
Reference in New Issue
Block a user