From 717bb60b2c0451369864b0a6c1e41c2e3f5d8b4d Mon Sep 17 00:00:00 2001 From: Jesse Wilson at Work Date: Wed, 20 Nov 2024 15:07:03 -0500 Subject: [PATCH] Host-side API for view-owned widgets (#2458) * Host-side API for view-owned widgets This introduces a new mutable property, windowInsets, that is propagated to the composition where it can be consumed. We put the burden on the user of RedwoodView to set this value to something non-zero if they have insets to be consumed. I've added an accelerator on Android for this, RedwoodLayout.consumeWindowInsets(mask: Int) * Spotless --- .../api/android/redwood-runtime.api | 7 ++-- redwood-runtime/api/jvm/redwood-runtime.api | 7 ++-- redwood-runtime/api/redwood-runtime.klib.api | 5 ++- .../app/cash/redwood/ui/UiConfiguration.kt | 34 ++++++++++++++++--- .../treehouse/composeui/TreehouseContent.kt | 2 ++ .../redwood/treehouse/TreehouseLayoutTest.kt | 9 +++-- .../app/cash/redwood/widget/RedwoodLayout.kt | 17 +++++++--- .../app/cash/redwood/widget/RedwoodUIView.kt | 5 +++ .../redwood/widget/RedwoodHTMLElementView.kt | 22 +++--------- 9 files changed, 74 insertions(+), 34 deletions(-) diff --git a/redwood-runtime/api/android/redwood-runtime.api b/redwood-runtime/api/android/redwood-runtime.api index e5b2eb78fa..ce4e3eabb6 100644 --- a/redwood-runtime/api/android/redwood-runtime.api +++ b/redwood-runtime/api/android/redwood-runtime.api @@ -245,13 +245,16 @@ public final class app/cash/redwood/ui/UiConfiguration { public static final field $stable I public static final field Companion Lapp/cash/redwood/ui/UiConfiguration$Companion; public fun ()V - public fun (ZLapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Size;DLapp/cash/redwood/ui/LayoutDirection;)V - public synthetic fun (ZLapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Size;DLapp/cash/redwood/ui/LayoutDirection;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (ZLapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Size;DLapp/cash/redwood/ui/LayoutDirection;)V + public synthetic fun (ZLapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Size;DLapp/cash/redwood/ui/LayoutDirection;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun copy (ZLapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Size;DLapp/cash/redwood/ui/LayoutDirection;)Lapp/cash/redwood/ui/UiConfiguration; + public static synthetic fun copy$default (Lapp/cash/redwood/ui/UiConfiguration;ZLapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Size;DLapp/cash/redwood/ui/LayoutDirection;ILjava/lang/Object;)Lapp/cash/redwood/ui/UiConfiguration; public fun equals (Ljava/lang/Object;)Z public final fun getDarkMode ()Z public final fun getDensity ()D public final fun getLayoutDirection ()Lapp/cash/redwood/ui/LayoutDirection; public final fun getSafeAreaInsets ()Lapp/cash/redwood/ui/Margin; + public final fun getViewInsets ()Lapp/cash/redwood/ui/Margin; public final fun getViewportSize ()Lapp/cash/redwood/ui/Size; public fun hashCode ()I public fun toString ()Ljava/lang/String; diff --git a/redwood-runtime/api/jvm/redwood-runtime.api b/redwood-runtime/api/jvm/redwood-runtime.api index 4e2c174787..74d00bab2b 100644 --- a/redwood-runtime/api/jvm/redwood-runtime.api +++ b/redwood-runtime/api/jvm/redwood-runtime.api @@ -241,13 +241,16 @@ public final class app/cash/redwood/ui/UiConfiguration { public static final field $stable I public static final field Companion Lapp/cash/redwood/ui/UiConfiguration$Companion; public fun ()V - public fun (ZLapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Size;DLapp/cash/redwood/ui/LayoutDirection;)V - public synthetic fun (ZLapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Size;DLapp/cash/redwood/ui/LayoutDirection;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (ZLapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Size;DLapp/cash/redwood/ui/LayoutDirection;)V + public synthetic fun (ZLapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Size;DLapp/cash/redwood/ui/LayoutDirection;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun copy (ZLapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Size;DLapp/cash/redwood/ui/LayoutDirection;)Lapp/cash/redwood/ui/UiConfiguration; + public static synthetic fun copy$default (Lapp/cash/redwood/ui/UiConfiguration;ZLapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Size;DLapp/cash/redwood/ui/LayoutDirection;ILjava/lang/Object;)Lapp/cash/redwood/ui/UiConfiguration; public fun equals (Ljava/lang/Object;)Z public final fun getDarkMode ()Z public final fun getDensity ()D public final fun getLayoutDirection ()Lapp/cash/redwood/ui/LayoutDirection; public final fun getSafeAreaInsets ()Lapp/cash/redwood/ui/Margin; + public final fun getViewInsets ()Lapp/cash/redwood/ui/Margin; public final fun getViewportSize ()Lapp/cash/redwood/ui/Size; public fun hashCode ()I public fun toString ()Ljava/lang/String; diff --git a/redwood-runtime/api/redwood-runtime.klib.api b/redwood-runtime/api/redwood-runtime.klib.api index e518000307..320e2ab60a 100644 --- a/redwood-runtime/api/redwood-runtime.klib.api +++ b/redwood-runtime/api/redwood-runtime.klib.api @@ -142,7 +142,7 @@ final class app.cash.redwood.ui/Size { // app.cash.redwood.ui/Size|null[0] } final class app.cash.redwood.ui/UiConfiguration { // app.cash.redwood.ui/UiConfiguration|null[0] - constructor (kotlin/Boolean = ..., app.cash.redwood.ui/Margin = ..., app.cash.redwood.ui/Size? = ..., kotlin/Double = ..., app.cash.redwood.ui/LayoutDirection = ...) // app.cash.redwood.ui/UiConfiguration.|(kotlin.Boolean;app.cash.redwood.ui.Margin;app.cash.redwood.ui.Size?;kotlin.Double;app.cash.redwood.ui.LayoutDirection){}[0] + constructor (kotlin/Boolean = ..., app.cash.redwood.ui/Margin = ..., app.cash.redwood.ui/Margin = ..., app.cash.redwood.ui/Size? = ..., kotlin/Double = ..., app.cash.redwood.ui/LayoutDirection = ...) // app.cash.redwood.ui/UiConfiguration.|(kotlin.Boolean;app.cash.redwood.ui.Margin;app.cash.redwood.ui.Margin;app.cash.redwood.ui.Size?;kotlin.Double;app.cash.redwood.ui.LayoutDirection){}[0] final val darkMode // app.cash.redwood.ui/UiConfiguration.darkMode|{}darkMode[0] final fun (): kotlin/Boolean // app.cash.redwood.ui/UiConfiguration.darkMode.|(){}[0] @@ -152,9 +152,12 @@ final class app.cash.redwood.ui/UiConfiguration { // app.cash.redwood.ui/UiConfi final fun (): app.cash.redwood.ui/LayoutDirection // app.cash.redwood.ui/UiConfiguration.layoutDirection.|(){}[0] final val safeAreaInsets // app.cash.redwood.ui/UiConfiguration.safeAreaInsets|{}safeAreaInsets[0] final fun (): app.cash.redwood.ui/Margin // app.cash.redwood.ui/UiConfiguration.safeAreaInsets.|(){}[0] + final val viewInsets // app.cash.redwood.ui/UiConfiguration.viewInsets|{}viewInsets[0] + final fun (): app.cash.redwood.ui/Margin // app.cash.redwood.ui/UiConfiguration.viewInsets.|(){}[0] final val viewportSize // app.cash.redwood.ui/UiConfiguration.viewportSize|{}viewportSize[0] final fun (): app.cash.redwood.ui/Size? // app.cash.redwood.ui/UiConfiguration.viewportSize.|(){}[0] + final fun copy(kotlin/Boolean = ..., app.cash.redwood.ui/Margin = ..., app.cash.redwood.ui/Margin = ..., app.cash.redwood.ui/Size? = ..., kotlin/Double = ..., app.cash.redwood.ui/LayoutDirection = ...): app.cash.redwood.ui/UiConfiguration // app.cash.redwood.ui/UiConfiguration.copy|copy(kotlin.Boolean;app.cash.redwood.ui.Margin;app.cash.redwood.ui.Margin;app.cash.redwood.ui.Size?;kotlin.Double;app.cash.redwood.ui.LayoutDirection){}[0] final fun equals(kotlin/Any?): kotlin/Boolean // app.cash.redwood.ui/UiConfiguration.equals|equals(kotlin.Any?){}[0] final fun hashCode(): kotlin/Int // app.cash.redwood.ui/UiConfiguration.hashCode|hashCode(){}[0] final fun toString(): kotlin/String // app.cash.redwood.ui/UiConfiguration.toString|toString(){}[0] diff --git a/redwood-runtime/src/commonMain/kotlin/app/cash/redwood/ui/UiConfiguration.kt b/redwood-runtime/src/commonMain/kotlin/app/cash/redwood/ui/UiConfiguration.kt index 2514464156..5a78c5cdcf 100644 --- a/redwood-runtime/src/commonMain/kotlin/app/cash/redwood/ui/UiConfiguration.kt +++ b/redwood-runtime/src/commonMain/kotlin/app/cash/redwood/ui/UiConfiguration.kt @@ -25,13 +25,23 @@ public class UiConfiguration( * True if the device is configured to use a dark color palette. */ public val darkMode: Boolean = false, + /** - * A set of distances from the edges of the display where system UI and other elements will be - * drawn. - * - * Use this margin to inset your content to avoid drawing under system UI elements. + * The insets of the host window, independent of where the Redwood composition is positioned + * within it. The Redwood composition is not responsible for consuming these insets. */ public val safeAreaInsets: Margin = Margin.Zero, + + /** + * The insets of the viewport that the composition is responsible for consuming. + * + * This may be zero if the host view isn't attached to a view hierarchy and therefore doesn't + * know its insets. + * + * See https://developer.android.com/develop/ui/views/layout/edge-to-edge + */ + public val viewInsets: Margin = Margin.Zero, + /** * The size of the viewport into which the composition is rendering. This could be as lage as the * entire screen or as small as an individual view within a larger native screen. @@ -55,5 +65,21 @@ public class UiConfiguration( */ public val layoutDirection: LayoutDirection = LayoutDirection.Ltr, ) { + public fun copy( + darkMode: Boolean = this.darkMode, + safeAreaInsets: Margin = this.safeAreaInsets, + viewInsets: Margin = this.viewInsets, + viewportSize: Size? = this.viewportSize, + density: Double = this.density, + layoutDirection: LayoutDirection = this.layoutDirection, + ): UiConfiguration = UiConfiguration( + darkMode = darkMode, + safeAreaInsets = safeAreaInsets, + viewInsets = viewInsets, + viewportSize = viewportSize, + density = density, + layoutDirection = layoutDirection, + ) + public companion object } diff --git a/redwood-treehouse-host-composeui/src/commonMain/kotlin/app/cash/redwood/treehouse/composeui/TreehouseContent.kt b/redwood-treehouse-host-composeui/src/commonMain/kotlin/app/cash/redwood/treehouse/composeui/TreehouseContent.kt index 4d6ca1ac9e..06099a1bbb 100644 --- a/redwood-treehouse-host-composeui/src/commonMain/kotlin/app/cash/redwood/treehouse/composeui/TreehouseContent.kt +++ b/redwood-treehouse-host-composeui/src/commonMain/kotlin/app/cash/redwood/treehouse/composeui/TreehouseContent.kt @@ -41,6 +41,7 @@ import app.cash.redwood.treehouse.TreehouseView.WidgetSystem import app.cash.redwood.treehouse.bindWhenReady import app.cash.redwood.ui.Density import app.cash.redwood.ui.LayoutDirection as RedwoodLayoutDirection +import app.cash.redwood.ui.Margin import app.cash.redwood.ui.OnBackPressedDispatcher import app.cash.redwood.ui.Size import app.cash.redwood.ui.UiConfiguration @@ -65,6 +66,7 @@ public fun TreehouseContent( val uiConfiguration = UiConfiguration( darkMode = isSystemInDarkTheme(), safeAreaInsets = safeAreaInsets(), + viewInsets = Margin.Zero, viewportSize = viewportSize, density = density.density.toDouble(), layoutDirection = when (LocalLayoutDirection.current) { diff --git a/redwood-treehouse-host/src/androidUnitTest/kotlin/app/cash/redwood/treehouse/TreehouseLayoutTest.kt b/redwood-treehouse-host/src/androidUnitTest/kotlin/app/cash/redwood/treehouse/TreehouseLayoutTest.kt index deefbd17e0..22e2185764 100644 --- a/redwood-treehouse-host/src/androidUnitTest/kotlin/app/cash/redwood/treehouse/TreehouseLayoutTest.kt +++ b/redwood-treehouse-host/src/androidUnitTest/kotlin/app/cash/redwood/treehouse/TreehouseLayoutTest.kt @@ -98,7 +98,10 @@ class TreehouseLayoutTest { @Test fun uiConfigurationEmitsSystemBarsSafeAreaInsetsChanges() = runTest { val layout = TreehouseLayout(activity, throwingWidgetSystem, activity.onBackPressedDispatcher) layout.uiConfiguration.test { - assertThat(awaitItem().safeAreaInsets).isEqualTo(Margin.Zero) + val value1 = awaitItem() + assertThat(value1.safeAreaInsets).isEqualTo(Margin.Zero) + assertThat(value1.viewInsets).isEqualTo(Margin.Zero) + val insets = Insets.of(10, 20, 30, 40) val windowInsets = WindowInsetsCompat.Builder() .setInsets(WindowInsetsCompat.Type.systemBars(), insets) @@ -112,7 +115,9 @@ class TreehouseLayoutTest { bottom = insets.bottom.toDp(), ) } - assertThat(awaitItem().safeAreaInsets).isEqualTo(expectedInsets) + val value2 = awaitItem() + assertThat(value2.safeAreaInsets).isEqualTo(Margin.Zero) + assertThat(value2.viewInsets).isEqualTo(expectedInsets) } } diff --git a/redwood-widget/src/androidMain/kotlin/app/cash/redwood/widget/RedwoodLayout.kt b/redwood-widget/src/androidMain/kotlin/app/cash/redwood/widget/RedwoodLayout.kt index fafc9cc4f6..a40157dd06 100644 --- a/redwood-widget/src/androidMain/kotlin/app/cash/redwood/widget/RedwoodLayout.kt +++ b/redwood-widget/src/androidMain/kotlin/app/cash/redwood/widget/RedwoodLayout.kt @@ -22,12 +22,12 @@ import android.view.View import android.view.ViewGroup import androidx.activity.OnBackPressedCallback as AndroidOnBackPressedCallback import androidx.activity.OnBackPressedDispatcher as AndroidOnBackPressedDispatcher -import androidx.core.graphics.Insets import androidx.core.view.children as viewGroupChildren import androidx.savedstate.findViewTreeSavedStateRegistryOwner import app.cash.redwood.ui.Cancellable import app.cash.redwood.ui.Density import app.cash.redwood.ui.LayoutDirection +import app.cash.redwood.ui.Margin import app.cash.redwood.ui.OnBackPressedCallback as RedwoodOnBackPressedCallback import app.cash.redwood.ui.OnBackPressedDispatcher as RedwoodOnBackPressedDispatcher import app.cash.redwood.ui.Size @@ -51,7 +51,11 @@ public open class RedwoodLayout( id = R.id.redwood_layout } - private val mutableUiConfiguration = MutableStateFlow(computeUiConfiguration()) + private val mutableUiConfiguration = MutableStateFlow( + computeUiConfiguration( + viewInsets = Margin.Zero, + ), + ) override val onBackPressedDispatcher: RedwoodOnBackPressedDispatcher = object : RedwoodOnBackPressedDispatcher { @@ -82,7 +86,9 @@ public open class RedwoodLayout( init { setOnWindowInsetsChangeListener { insets -> - mutableUiConfiguration.value = computeUiConfiguration(insets = insets.safeDrawing) + mutableUiConfiguration.value = computeUiConfiguration( + viewInsets = insets.safeDrawing.toMargin(Density(resources)), + ) } } @@ -112,7 +118,7 @@ public open class RedwoodLayout( private fun computeUiConfiguration( config: Configuration = context.resources.configuration, - insets: Insets = rootWindowInsetsCompat.safeDrawing, + viewInsets: Margin = uiConfiguration.value.viewInsets, ): UiConfiguration { val viewportSize: Size val density: Double @@ -122,7 +128,8 @@ public open class RedwoodLayout( } return UiConfiguration( darkMode = (config.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES, - safeAreaInsets = insets.toMargin(Density(resources)), + safeAreaInsets = Margin.Zero, + viewInsets = viewInsets, viewportSize = viewportSize, density = density, layoutDirection = when (config.layoutDirection) { diff --git a/redwood-widget/src/iosMain/kotlin/app/cash/redwood/widget/RedwoodUIView.kt b/redwood-widget/src/iosMain/kotlin/app/cash/redwood/widget/RedwoodUIView.kt index ff522aef5d..26814ca2a0 100644 --- a/redwood-widget/src/iosMain/kotlin/app/cash/redwood/widget/RedwoodUIView.kt +++ b/redwood-widget/src/iosMain/kotlin/app/cash/redwood/widget/RedwoodUIView.kt @@ -58,6 +58,7 @@ public open class RedwoodUIView : RedwoodView { MutableStateFlow( computeUiConfiguration( traitCollection = valueRootView.traitCollection, + viewInsets = Margin.Zero, layoutDirection = valueRootView.effectiveUserInterfaceLayoutDirection, bounds = valueRootView.bounds, ), @@ -78,8 +79,10 @@ public open class RedwoodUIView : RedwoodView { get() = null private fun updateUiConfiguration() { + val old = mutableUiConfiguration.value mutableUiConfiguration.value = computeUiConfiguration( traitCollection = valueRootView.traitCollection, + viewInsets = old.viewInsets, layoutDirection = valueRootView.effectiveUserInterfaceLayoutDirection, bounds = valueRootView.bounds, ) @@ -124,12 +127,14 @@ public open class RedwoodUIView : RedwoodView { internal fun computeUiConfiguration( traitCollection: UITraitCollection, + viewInsets: Margin, layoutDirection: UIUserInterfaceLayoutDirection, bounds: CValue, ): UiConfiguration { return UiConfiguration( darkMode = traitCollection.userInterfaceStyle == UIUserInterfaceStyle.UIUserInterfaceStyleDark, safeAreaInsets = computeSafeAreaInsets(), + viewInsets = viewInsets, viewportSize = bounds.useContents { with(Density.Default) { Size(size.width.toDp(), size.height.toDp()) diff --git a/redwood-widget/src/jsMain/kotlin/app/cash/redwood/widget/RedwoodHTMLElementView.kt b/redwood-widget/src/jsMain/kotlin/app/cash/redwood/widget/RedwoodHTMLElementView.kt index c982535ce0..4b2619e6b2 100644 --- a/redwood-widget/src/jsMain/kotlin/app/cash/redwood/widget/RedwoodHTMLElementView.kt +++ b/redwood-widget/src/jsMain/kotlin/app/cash/redwood/widget/RedwoodHTMLElementView.kt @@ -17,6 +17,7 @@ package app.cash.redwood.widget import app.cash.redwood.ui.Cancellable import app.cash.redwood.ui.LayoutDirection +import app.cash.redwood.ui.Margin import app.cash.redwood.ui.OnBackPressedCallback import app.cash.redwood.ui.OnBackPressedDispatcher import app.cash.redwood.ui.Size @@ -64,6 +65,7 @@ private class RedwoodHTMLElementView( _uiConfiguration = MutableStateFlow( UiConfiguration( darkMode = colorSchemeQuery.matches, + viewInsets = Margin.Zero, viewportSize = Size(width = value.offsetWidth.dp, height = value.offsetHeight.dp), layoutDirection = when (value.dir) { "ltr" -> LayoutDirection.Ltr @@ -75,15 +77,7 @@ private class RedwoodHTMLElementView( ) colorSchemeQuery.addEventListener("change", { event -> - updateUiConfiguration { old -> - UiConfiguration( - darkMode = event.unsafeCast().matches, - safeAreaInsets = old.safeAreaInsets, - viewportSize = old.viewportSize, - density = old.density, - layoutDirection = old.layoutDirection, - ) - } + updateUiConfiguration { it.copy(darkMode = event.unsafeCast().matches) } }) observePixelRatioChange() @@ -107,15 +101,7 @@ private class RedwoodHTMLElementView( pixelRatioQuery.removeEventListener("change", listener) } - updateUiConfiguration { old -> - UiConfiguration( - darkMode = old.darkMode, - safeAreaInsets = old.safeAreaInsets, - viewportSize = old.viewportSize, - density = window.devicePixelRatio, - layoutDirection = old.layoutDirection, - ) - } + updateUiConfiguration { it.copy(density = window.devicePixelRatio) } } private fun updateUiConfiguration(updater: (UiConfiguration) -> UiConfiguration) {