Skip to content

Commit

Permalink
Add support for an onScroll property to Row and Column. (#2067)
Browse files Browse the repository at this point in the history
* Add support for an onScroll property to Row and Column.

* Update changelog.

* Add tests.

* Update API.

* Update redwood-layout-view/src/main/kotlin/app/cash/redwood/layout/view/ViewFlexContainer.kt

Co-authored-by: Jake Wharton <[email protected]>

* Update redwood-layout-view/src/main/kotlin/app/cash/redwood/layout/view/ViewFlexContainer.kt

Co-authored-by: Jake Wharton <[email protected]>

* Fixes.

* Lint.

* Fixes.

* Update redwood api.

* Add screenshots.

* Update API.

* Fix screenshot test.

* Enforce size.

* Update changelog.

---------

Co-authored-by: Jake Wharton <[email protected]>
  • Loading branch information
colinrtwhite and JakeWharton authored Jul 24, 2024
1 parent 03730ba commit 7a040af
Show file tree
Hide file tree
Showing 31 changed files with 271 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

New:
- Wasm JS added as a target for common Redwood modules. There is no Treehouse support today.
- Add `onScroll` property to `Row` and `Column`. This property is invoked when `overflow = Overflow.Scroll` and the container is scrolled.

Changed:
- The `TreehouseApp` type is now an abstract class. This should make it easier to write unit tests for code that integrates Treehouse.
Expand Down
4 changes: 2 additions & 2 deletions redwood-layout-compose/api/redwood-layout-compose.api
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public abstract interface class app/cash/redwood/layout/compose/BoxScope {
}

public final class app/cash/redwood/layout/compose/ColumnKt {
public static final fun Column-n_TNbJ4 (IILapp/cash/redwood/ui/Margin;IIILapp/cash/redwood/Modifier;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
public static final fun Column-TKMhg4E (IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Lapp/cash/redwood/Modifier;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
}

public abstract interface class app/cash/redwood/layout/compose/ColumnScope {
Expand All @@ -27,7 +27,7 @@ public abstract interface class app/cash/redwood/layout/compose/ColumnScope {
}

public final class app/cash/redwood/layout/compose/RowKt {
public static final fun Row-dhzo6EI (IILapp/cash/redwood/ui/Margin;IIILapp/cash/redwood/Modifier;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
public static final fun Row-5pIFpiI (IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Lapp/cash/redwood/Modifier;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
}

public abstract interface class app/cash/redwood/layout/compose/RowScope {
Expand Down
4 changes: 2 additions & 2 deletions redwood-layout-compose/api/redwood-layout-compose.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,6 @@ final val app.cash.redwood.layout.compose/app_cash_redwood_layout_compose_Vertic
final val app.cash.redwood.layout.compose/app_cash_redwood_layout_compose_WidthImpl$stableprop // app.cash.redwood.layout.compose/app_cash_redwood_layout_compose_WidthImpl$stableprop|#static{}app_cash_redwood_layout_compose_WidthImpl$stableprop[0]

final fun app.cash.redwood.layout.compose/Box(app.cash.redwood.layout.api/Constraint?, app.cash.redwood.layout.api/Constraint?, app.cash.redwood.ui/Margin?, app.cash.redwood.layout.api/CrossAxisAlignment?, app.cash.redwood.layout.api/CrossAxisAlignment?, app.cash.redwood/Modifier?, kotlin/Function3<app.cash.redwood.layout.compose/BoxScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // app.cash.redwood.layout.compose/Box|Box(app.cash.redwood.layout.api.Constraint?;app.cash.redwood.layout.api.Constraint?;app.cash.redwood.ui.Margin?;app.cash.redwood.layout.api.CrossAxisAlignment?;app.cash.redwood.layout.api.CrossAxisAlignment?;app.cash.redwood.Modifier?;kotlin.Function3<app.cash.redwood.layout.compose.BoxScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun app.cash.redwood.layout.compose/Column(app.cash.redwood.layout.api/Constraint?, app.cash.redwood.layout.api/Constraint?, app.cash.redwood.ui/Margin?, app.cash.redwood.layout.api/Overflow?, app.cash.redwood.layout.api/CrossAxisAlignment?, app.cash.redwood.layout.api/MainAxisAlignment?, app.cash.redwood/Modifier?, kotlin/Function3<app.cash.redwood.layout.compose/ColumnScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // app.cash.redwood.layout.compose/Column|Column(app.cash.redwood.layout.api.Constraint?;app.cash.redwood.layout.api.Constraint?;app.cash.redwood.ui.Margin?;app.cash.redwood.layout.api.Overflow?;app.cash.redwood.layout.api.CrossAxisAlignment?;app.cash.redwood.layout.api.MainAxisAlignment?;app.cash.redwood.Modifier?;kotlin.Function3<app.cash.redwood.layout.compose.ColumnScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun app.cash.redwood.layout.compose/Row(app.cash.redwood.layout.api/Constraint?, app.cash.redwood.layout.api/Constraint?, app.cash.redwood.ui/Margin?, app.cash.redwood.layout.api/Overflow?, app.cash.redwood.layout.api/MainAxisAlignment?, app.cash.redwood.layout.api/CrossAxisAlignment?, app.cash.redwood/Modifier?, kotlin/Function3<app.cash.redwood.layout.compose/RowScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // app.cash.redwood.layout.compose/Row|Row(app.cash.redwood.layout.api.Constraint?;app.cash.redwood.layout.api.Constraint?;app.cash.redwood.ui.Margin?;app.cash.redwood.layout.api.Overflow?;app.cash.redwood.layout.api.MainAxisAlignment?;app.cash.redwood.layout.api.CrossAxisAlignment?;app.cash.redwood.Modifier?;kotlin.Function3<app.cash.redwood.layout.compose.RowScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun app.cash.redwood.layout.compose/Column(app.cash.redwood.layout.api/Constraint?, app.cash.redwood.layout.api/Constraint?, app.cash.redwood.ui/Margin?, app.cash.redwood.layout.api/Overflow?, app.cash.redwood.layout.api/CrossAxisAlignment?, app.cash.redwood.layout.api/MainAxisAlignment?, kotlin/Function1<kotlin/Double, kotlin/Unit>?, app.cash.redwood/Modifier?, kotlin/Function3<app.cash.redwood.layout.compose/ColumnScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // app.cash.redwood.layout.compose/Column|Column(app.cash.redwood.layout.api.Constraint?;app.cash.redwood.layout.api.Constraint?;app.cash.redwood.ui.Margin?;app.cash.redwood.layout.api.Overflow?;app.cash.redwood.layout.api.CrossAxisAlignment?;app.cash.redwood.layout.api.MainAxisAlignment?;kotlin.Function1<kotlin.Double,kotlin.Unit>?;app.cash.redwood.Modifier?;kotlin.Function3<app.cash.redwood.layout.compose.ColumnScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun app.cash.redwood.layout.compose/Row(app.cash.redwood.layout.api/Constraint?, app.cash.redwood.layout.api/Constraint?, app.cash.redwood.ui/Margin?, app.cash.redwood.layout.api/Overflow?, app.cash.redwood.layout.api/MainAxisAlignment?, app.cash.redwood.layout.api/CrossAxisAlignment?, kotlin/Function1<kotlin/Double, kotlin/Unit>?, app.cash.redwood/Modifier?, kotlin/Function3<app.cash.redwood.layout.compose/RowScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // app.cash.redwood.layout.compose/Row|Row(app.cash.redwood.layout.api.Constraint?;app.cash.redwood.layout.api.Constraint?;app.cash.redwood.ui.Margin?;app.cash.redwood.layout.api.Overflow?;app.cash.redwood.layout.api.MainAxisAlignment?;app.cash.redwood.layout.api.CrossAxisAlignment?;kotlin.Function1<kotlin.Double,kotlin.Unit>?;app.cash.redwood.Modifier?;kotlin.Function3<app.cash.redwood.layout.compose.RowScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun app.cash.redwood.layout.compose/Spacer(app.cash.redwood.ui/Dp, app.cash.redwood.ui/Dp, app.cash.redwood/Modifier?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // app.cash.redwood.layout.compose/Spacer|Spacer(app.cash.redwood.ui.Dp;app.cash.redwood.ui.Dp;app.cash.redwood.Modifier?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import app.cash.redwood.yoga.FlexDirection
import com.android.resources.LayoutDirection
import com.google.testing.junit.testparameterinjector.TestParameter
import com.google.testing.junit.testparameterinjector.TestParameterInjector
import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.runner.RunWith

Expand Down Expand Up @@ -69,13 +70,25 @@ class ComposeUiFlexContainerTest(
) : TestFlexContainer<@Composable () -> Unit>,
FlexContainer<@Composable () -> Unit> by delegate {
private var childCount = 0

override val children: ComposeWidgetChildren = delegate.children

constructor(direction: FlexDirection, backgroundColor: Int) : this(
ComposeUiFlexContainer(direction).apply {
testOnlyModifier = Modifier.background(Color(backgroundColor))
},
)

override fun onScroll(onScroll: ((Double) -> Unit)?) {
delegate.onScroll(onScroll)
}

override fun scroll(offset: Double) {
runBlocking {
delegate.scrollState?.scrollTo(offset.toInt())
}
}

override fun add(widget: Widget<@Composable () -> Unit>) {
addAt(childCount, widget)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package app.cash.redwood.layout.composeui

import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
Expand All @@ -23,9 +24,12 @@ import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand Down Expand Up @@ -65,9 +69,11 @@ internal class ComposeUiFlexContainer(
private var height by mutableStateOf(Constraint.Wrap)
private var overflow by mutableStateOf(Overflow.Clip)
private var margin by mutableStateOf(Margin.Zero)
private var onScroll: ((Double) -> Unit)? by mutableStateOf(null)
override var density = Density(1.0)

internal var testOnlyModifier: Modifier? = null
internal var scrollState: ScrollState? = null

override fun width(width: Constraint) {
this.width = width
Expand All @@ -85,6 +91,10 @@ internal class ComposeUiFlexContainer(
this.overflow = overflow
}

override fun onScroll(onScroll: ((Double) -> Unit)?) {
this.onScroll = onScroll
}

override fun crossAxisAlignment(crossAxisAlignment: CrossAxisAlignment) {
super.crossAxisAlignment(crossAxisAlignment)
invalidate()
Expand Down Expand Up @@ -136,16 +146,29 @@ internal class ComposeUiFlexContainer(
modifier.wrapContentHeight(Alignment.Top, unbounded = true)
}
if (overflow == Overflow.Scroll) {
val scrollState = rememberScrollState().also { scrollState = it }
if (flexDirection.isHorizontal) {
modifier = modifier.horizontalScroll(rememberScrollState())
modifier = modifier.horizontalScroll(scrollState)
} else {
modifier = modifier.verticalScroll(rememberScrollState())
modifier = modifier.verticalScroll(scrollState)
}
ObserveScrollState(scrollState)
}
testOnlyModifier?.let { modifier = modifier.then(it) }
return modifier
}

@Composable
private fun ObserveScrollState(scrollState: ScrollState) {
val onScroll = onScroll
if (onScroll != null) {
val offset by remember { derivedStateOf { scrollState.value.toDouble() } }
LaunchedEffect(offset) {
onScroll(offset)
}
}
}

private fun measure(
scope: MeasureScope,
measurables: List<Measurable>,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import app.cash.redwood.widget.Widget
import org.w3c.dom.Document
import org.w3c.dom.HTMLDivElement
import org.w3c.dom.HTMLElement
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventListener

public class HTMLElementRedwoodLayoutWidgetFactory(
private val document: Document,
Expand Down Expand Up @@ -80,6 +82,8 @@ private class HTMLFlexContainer(

override val children: Widget.Children<HTMLElement> = HTMLFlexElementChildren(value)

private var scrollEventListener: EventListener? = null

override fun width(width: Constraint) {
value.style.width = width.toCss()
}
Expand All @@ -101,6 +105,27 @@ private class HTMLFlexContainer(
value.overflowSetter(overflow.toCss())
}

override fun onScroll(onScroll: ((Double) -> Unit)?) {
scrollEventListener?.let { eventListener ->
value.removeEventListener("scroll", eventListener)
scrollEventListener = null
}

if (onScroll != null) {
val eventListener = object : EventListener {
override fun handleEvent(event: Event) {
val offset = when (value.style.flexDirection) {
"row" -> value.scrollTop
"column" -> value.scrollLeft
else -> throw AssertionError()
}
onScroll(offset)
}
}.also { scrollEventListener = it }
value.addEventListener("scroll", eventListener)
}
}

override fun crossAxisAlignment(crossAxisAlignment: CrossAxisAlignment) {
value.style.alignItems = crossAxisAlignment.toCss()
}
Expand Down
16 changes: 10 additions & 6 deletions redwood-layout-schema/api/redwood-layout-schema.api
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,23 @@ public final class app/cash/redwood/layout/BoxScope {
}

public final class app/cash/redwood/layout/Column {
public synthetic fun <init> (IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1-jfchsNQ ()I
public final fun component2-jfchsNQ ()I
public final fun component3 ()Lapp/cash/redwood/ui/Margin;
public final fun component4-Andb1w4 ()I
public final fun component5-eoX4IBc ()I
public final fun component6-Ng1ngeI ()I
public final fun component7 ()Lkotlin/jvm/functions/Function1;
public final fun copy-bVM0h8I (IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;)Lapp/cash/redwood/layout/Column;
public static synthetic fun copy-bVM0h8I$default (Lapp/cash/redwood/layout/Column;IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/cash/redwood/layout/Column;
public final fun component8 ()Lkotlin/jvm/functions/Function1;
public final fun copy-9Hx0b_M (IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lapp/cash/redwood/layout/Column;
public static synthetic fun copy-9Hx0b_M$default (Lapp/cash/redwood/layout/Column;IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/cash/redwood/layout/Column;
public fun equals (Ljava/lang/Object;)Z
public final fun getChildren ()Lkotlin/jvm/functions/Function1;
public final fun getHeight-jfchsNQ ()I
public final fun getHorizontalAlignment-eoX4IBc ()I
public final fun getMargin ()Lapp/cash/redwood/ui/Margin;
public final fun getOnScroll ()Lkotlin/jvm/functions/Function1;
public final fun getOverflow-Andb1w4 ()I
public final fun getVerticalAlignment-Ng1ngeI ()I
public final fun getWidth-jfchsNQ ()I
Expand Down Expand Up @@ -109,21 +111,23 @@ public abstract interface class app/cash/redwood/layout/RedwoodLayout {
}

public final class app/cash/redwood/layout/Row {
public synthetic fun <init> (IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1-jfchsNQ ()I
public final fun component2-jfchsNQ ()I
public final fun component3 ()Lapp/cash/redwood/ui/Margin;
public final fun component4-Andb1w4 ()I
public final fun component5-Ng1ngeI ()I
public final fun component6-eoX4IBc ()I
public final fun component7 ()Lkotlin/jvm/functions/Function1;
public final fun copy-DTaUb5E (IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;)Lapp/cash/redwood/layout/Row;
public static synthetic fun copy-DTaUb5E$default (Lapp/cash/redwood/layout/Row;IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/cash/redwood/layout/Row;
public final fun component8 ()Lkotlin/jvm/functions/Function1;
public final fun copy-IkXdkrs (IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lapp/cash/redwood/layout/Row;
public static synthetic fun copy-IkXdkrs$default (Lapp/cash/redwood/layout/Row;IILapp/cash/redwood/ui/Margin;IIILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/cash/redwood/layout/Row;
public fun equals (Ljava/lang/Object;)Z
public final fun getChildren ()Lkotlin/jvm/functions/Function1;
public final fun getHeight-jfchsNQ ()I
public final fun getHorizontalAlignment-Ng1ngeI ()I
public final fun getMargin ()Lapp/cash/redwood/ui/Margin;
public final fun getOnScroll ()Lkotlin/jvm/functions/Function1;
public final fun getOverflow-Andb1w4 ()I
public final fun getVerticalAlignment-eoX4IBc ()I
public final fun getWidth-jfchsNQ ()I
Expand Down
2 changes: 2 additions & 0 deletions redwood-layout-schema/redwood-api.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<property tag="4" name="overflow" type="app.cash.redwood.layout.api.Overflow"/>
<property tag="5" name="horizontalAlignment" type="app.cash.redwood.layout.api.MainAxisAlignment"/>
<property tag="6" name="verticalAlignment" type="app.cash.redwood.layout.api.CrossAxisAlignment"/>
<event tag="7" name="onScroll" params="kotlin.Double" nullable="true"/>
<children tag="1" name="children"/>
</widget>
<widget tag="2" type="app.cash.redwood.layout.Column">
Expand All @@ -15,6 +16,7 @@
<property tag="4" name="overflow" type="app.cash.redwood.layout.api.Overflow"/>
<property tag="5" name="horizontalAlignment" type="app.cash.redwood.layout.api.CrossAxisAlignment"/>
<property tag="6" name="verticalAlignment" type="app.cash.redwood.layout.api.MainAxisAlignment"/>
<event tag="7" name="onScroll" params="kotlin.Double" nullable="true"/>
<children tag="1" name="children"/>
</widget>
<widget tag="3" type="app.cash.redwood.layout.Spacer">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,16 @@ public data class Row(
@Default("CrossAxisAlignment.Start")
val verticalAlignment: CrossAxisAlignment,

/**
* Invoked when the container scrolls. The function's `offset` is represented in units in the
* host's coordinate system.
*
* @see Overflow.Scroll
*/
@Property(7)
@Default("null")
val onScroll: ((offset: Double) -> Unit)?,

/**
* A slot to add widgets in.
*/
Expand Down Expand Up @@ -142,6 +152,16 @@ public data class Column(
@Default("MainAxisAlignment.Start")
val verticalAlignment: MainAxisAlignment,

/**
* Invoked when the container scrolls. The function's `offset` is represented in units in the
* host's coordinate system.
*
* @see Overflow.Scroll
*/
@Property(7)
@Default("null")
val onScroll: ((offset: Double) -> Unit)?,

/**
* A slot to add widgets in.
*/
Expand Down
Loading

0 comments on commit 7a040af

Please sign in to comment.