diff --git a/redwood-lazylayout-compose/src/commonTest/kotlin/app/cash/redwood/lazylayout/compose/LazyListTest.kt b/redwood-lazylayout-compose/src/commonTest/kotlin/app/cash/redwood/lazylayout/compose/LazyListTest.kt index ce7376a273..f0a1b6309f 100644 --- a/redwood-lazylayout-compose/src/commonTest/kotlin/app/cash/redwood/lazylayout/compose/LazyListTest.kt +++ b/redwood-lazylayout-compose/src/commonTest/kotlin/app/cash/redwood/lazylayout/compose/LazyListTest.kt @@ -16,19 +16,31 @@ package app.cash.redwood.lazylayout.compose import app.cash.redwood.Modifier +import app.cash.redwood.RedwoodCodegenApi import app.cash.redwood.layout.api.Constraint import app.cash.redwood.layout.api.CrossAxisAlignment +import app.cash.redwood.layout.widget.RedwoodLayoutTestingWidgetFactory import app.cash.redwood.lazylayout.api.ScrollItemIndex import app.cash.redwood.lazylayout.widget.LazyListValue +import app.cash.redwood.lazylayout.widget.ListUpdateCallback +import app.cash.redwood.lazylayout.widget.RedwoodLazyLayoutTestingWidgetFactory +import app.cash.redwood.lazylayout.widget.RedwoodLazyLayoutWidgetFactory +import app.cash.redwood.lazylayout.widget.WindowedLazyList +import app.cash.redwood.testing.TestRedwoodComposition import app.cash.redwood.testing.WidgetValue import app.cash.redwood.ui.Margin +import app.cash.redwood.ui.UiConfiguration import app.cash.redwood.ui.dp +import app.cash.redwood.widget.MutableListChildren import assertk.assertThat import assertk.assertions.containsExactly import com.example.redwood.testing.compose.Text -import com.example.redwood.testing.widget.TestSchemaTester +import com.example.redwood.testing.widget.TestSchemaTestingWidgetFactory +import com.example.redwood.testing.widget.TestSchemaWidgetFactories import com.example.redwood.testing.widget.TextValue import kotlin.test.Test +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest class LazyListTest { @@ -86,6 +98,136 @@ class LazyListTest { ), ) } + + @OptIn(RedwoodCodegenApi::class) + @Test + fun scrollPopulatedLazyColumn() = runTest { + val windowedLazyList = FakeWindowedLazyList() + val factories = TestSchemaWidgetFactories( + TestSchema = TestSchemaTestingWidgetFactory(), + RedwoodLayout = RedwoodLayoutTestingWidgetFactory(), + RedwoodLazyLayout = RedwoodLazyLayoutFakeWidgetFactory(windowedLazyList), + ) + val snapshot = TestSchemaTester(factories) { + setContent { + LazyColumn(placeholder = { Text("Placeholder") }) { + items(100) { + Text(it.toString()) + } + } + } + awaitSnapshot() + windowedLazyList.updateViewport(50, 60) + awaitSnapshot() + } + + assertThat(snapshot) + .containsExactly( + DefaultLazyListValue.copy( + itemsBefore = 35, + itemsAfter = 25, + placeholder = List(20) { TextValue(Modifier, "Placeholder") }, + items = List(40) { TextValue(Modifier, (it + 35).toString()) }, + ), + ) + } +} + +@OptIn(RedwoodCodegenApi::class) +private suspend fun TestSchemaTester( + factories: TestSchemaWidgetFactories = TestSchemaWidgetFactories( + TestSchema = TestSchemaTestingWidgetFactory(), + RedwoodLayout = RedwoodLayoutTestingWidgetFactory(), + RedwoodLazyLayout = RedwoodLazyLayoutTestingWidgetFactory(), + ), + body: suspend TestRedwoodComposition>.() -> R, +): R = + coroutineScope { + val container = MutableListChildren() + val tester = TestRedwoodComposition( + this, + factories, + container, + MutableStateFlow(UiConfiguration()), + ) { + container.map { it.value } + } + try { + tester.body() + } finally { + tester.cancel() + } + } + +private class RedwoodLazyLayoutFakeWidgetFactory( + private val windowedLazyList: WindowedLazyList, +) : RedwoodLazyLayoutWidgetFactory { + override fun LazyList() = windowedLazyList + override fun RefreshableLazyList() = error("unexpected call") +} + +private class FakeWindowedLazyList : WindowedLazyList(NoOpListUpdateCallback) { + override val `value`: WidgetValue + get() = LazyListValue( + modifier = modifier, + isVertical = isVertical!!, + onViewportChanged = onViewportChanged!!, + itemsBefore = items.itemsBefore, + itemsAfter = items.itemsAfter, + width = width!!, + height = height!!, + margin = margin!!, + crossAxisAlignment = crossAxisAlignment!!, + scrollItemIndex = scrollItemIndex!!, + placeholder = placeholder.map { it.`value` }, + items = items.filterNotNull().map { it.`value` }, + ) + + override var modifier: Modifier = Modifier + + private var isVertical: Boolean? = null + + private var width: Constraint? = null + + private var height: Constraint? = null + + private var margin: Margin? = null + + private var crossAxisAlignment: CrossAxisAlignment? = null + + private var scrollItemIndex: ScrollItemIndex? = null + + override val placeholder: MutableListChildren = MutableListChildren() + + override fun isVertical(isVertical: Boolean) { + this.isVertical = isVertical + } + + override fun width(width: Constraint) { + this.width = width + } + + override fun height(height: Constraint) { + this.height = height + } + + override fun margin(margin: Margin) { + this.margin = margin + } + + override fun crossAxisAlignment(crossAxisAlignment: CrossAxisAlignment) { + this.crossAxisAlignment = crossAxisAlignment + } + + override fun scrollItemIndex(scrollItemIndex: ScrollItemIndex) { + this.scrollItemIndex = scrollItemIndex + } +} + +private object NoOpListUpdateCallback : ListUpdateCallback { + override fun onInserted(position: Int, count: Int) = Unit + override fun onMoved(fromPosition: Int, toPosition: Int, count: Int) = Unit + override fun onRemoved(position: Int, count: Int) = Unit } private val DefaultLazyListValue = LazyListValue( diff --git a/redwood-lazylayout-widget/src/commonMain/kotlin/app/cash/redwood/lazylayout/widget/WindowedLazyList.kt b/redwood-lazylayout-widget/src/commonMain/kotlin/app/cash/redwood/lazylayout/widget/WindowedLazyList.kt index 163e4b5f90..4036f1c383 100644 --- a/redwood-lazylayout-widget/src/commonMain/kotlin/app/cash/redwood/lazylayout/widget/WindowedLazyList.kt +++ b/redwood-lazylayout-widget/src/commonMain/kotlin/app/cash/redwood/lazylayout/widget/WindowedLazyList.kt @@ -21,7 +21,7 @@ public abstract class WindowedLazyList( private var firstVisibleItemIndex = 0 private var lastVisibleItemIndex = 0 - private var onViewportChanged: ((firstVisibleItemIndex: Int, lastVisibleItemIndex: Int) -> Unit)? = null + protected var onViewportChanged: ((firstVisibleItemIndex: Int, lastVisibleItemIndex: Int) -> Unit)? = null final override val items: WindowedChildren = WindowedChildren(listUpdateCallback) @@ -29,7 +29,7 @@ public abstract class WindowedLazyList( this.onViewportChanged = onViewportChanged } - protected fun updateViewport(firstVisibleItemIndex: Int, lastVisibleItemIndex: Int) { + public fun updateViewport(firstVisibleItemIndex: Int, lastVisibleItemIndex: Int) { if (firstVisibleItemIndex != this.firstVisibleItemIndex || lastVisibleItemIndex != this.lastVisibleItemIndex) { this.firstVisibleItemIndex = firstVisibleItemIndex this.lastVisibleItemIndex = lastVisibleItemIndex