From c5b65db65e43011e22f1f566141827ff6aebe781 Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Mon, 18 Nov 2024 10:24:49 -0500 Subject: [PATCH] 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) --- .../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 ++++++++++++++-- .../redwood-treehouse-host-composeui.api | 2 +- .../jvm/redwood-treehouse-host-composeui.api | 2 +- .../redwood-treehouse-host-composeui.klib.api | 2 +- .../treehouse/composeui/TreehouseContent.kt | 4 ++ redwood-widget/api/android/redwood-widget.api | 6 +++ redwood-widget/api/jvm/redwood-widget.api | 2 + redwood-widget/api/redwood-widget.klib.api | 8 ++++ .../app/cash/redwood/widget/RedwoodLayout.kt | 39 +++++++++++++++++-- .../app/cash/redwood/widget/RedwoodView.kt | 7 ++++ .../app/cash/redwood/widget/RedwoodUIView.kt | 11 ++++++ .../redwood/widget/RedwoodHTMLElementView.kt | 28 +++++-------- 15 files changed, 130 insertions(+), 34 deletions(-) diff --git a/redwood-runtime/api/android/redwood-runtime.api b/redwood-runtime/api/android/redwood-runtime.api index e5b2eb78fa..7e07e2a529 100644 --- a/redwood-runtime/api/android/redwood-runtime.api +++ b/redwood-runtime/api/android/redwood-runtime.api @@ -245,14 +245,17 @@ 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 getViewportSize ()Lapp/cash/redwood/ui/Size; + public final fun getWindowInsets ()Lapp/cash/redwood/ui/Margin; 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..0025fb2513 100644 --- a/redwood-runtime/api/jvm/redwood-runtime.api +++ b/redwood-runtime/api/jvm/redwood-runtime.api @@ -241,14 +241,17 @@ 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 getViewportSize ()Lapp/cash/redwood/ui/Size; + public final fun getWindowInsets ()Lapp/cash/redwood/ui/Margin; 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..9ae04c6601 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] @@ -154,7 +154,10 @@ final class app.cash.redwood.ui/UiConfiguration { // app.cash.redwood.ui/UiConfi final fun (): app.cash.redwood.ui/Margin // app.cash.redwood.ui/UiConfiguration.safeAreaInsets.|(){}[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 val windowInsets // app.cash.redwood.ui/UiConfiguration.windowInsets|{}windowInsets[0] + final fun (): app.cash.redwood.ui/Margin // app.cash.redwood.ui/UiConfiguration.windowInsets.|(){}[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..1334ae6b56 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 windowInsets: 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, + windowInsets: Margin = this.windowInsets, + viewportSize: Size? = this.viewportSize, + density: Double = this.density, + layoutDirection: LayoutDirection = this.layoutDirection, + ): UiConfiguration = UiConfiguration( + darkMode = darkMode, + safeAreaInsets = safeAreaInsets, + windowInsets = windowInsets, + viewportSize = viewportSize, + density = density, + layoutDirection = layoutDirection, + ) + public companion object } diff --git a/redwood-treehouse-host-composeui/api/android/redwood-treehouse-host-composeui.api b/redwood-treehouse-host-composeui/api/android/redwood-treehouse-host-composeui.api index 1f70072d4e..0381b1395a 100644 --- a/redwood-treehouse-host-composeui/api/android/redwood-treehouse-host-composeui.api +++ b/redwood-treehouse-host-composeui/api/android/redwood-treehouse-host-composeui.api @@ -8,6 +8,6 @@ public final class app/cash/redwood/treehouse/composeui/ComposableSingletons$Emp } public final class app/cash/redwood/treehouse/composeui/TreehouseContentKt { - public static final fun TreehouseContent (Lapp/cash/redwood/treehouse/TreehouseApp;Lapp/cash/redwood/treehouse/TreehouseView$WidgetSystem;Lapp/cash/redwood/treehouse/TreehouseContentSource;Landroidx/compose/ui/Modifier;Lapp/cash/redwood/treehouse/DynamicContentWidgetFactory;Landroidx/compose/runtime/Composer;II)V + public static final fun TreehouseContent (Lapp/cash/redwood/treehouse/TreehouseApp;Lapp/cash/redwood/treehouse/TreehouseView$WidgetSystem;Lapp/cash/redwood/treehouse/TreehouseContentSource;Landroidx/compose/ui/Modifier;Lapp/cash/redwood/ui/Margin;Lapp/cash/redwood/treehouse/DynamicContentWidgetFactory;Landroidx/compose/runtime/Composer;II)V } diff --git a/redwood-treehouse-host-composeui/api/jvm/redwood-treehouse-host-composeui.api b/redwood-treehouse-host-composeui/api/jvm/redwood-treehouse-host-composeui.api index 607e240683..b4dfbe7cc7 100644 --- a/redwood-treehouse-host-composeui/api/jvm/redwood-treehouse-host-composeui.api +++ b/redwood-treehouse-host-composeui/api/jvm/redwood-treehouse-host-composeui.api @@ -8,6 +8,6 @@ public final class app/cash/redwood/treehouse/composeui/ComposableSingletons$Emp } public final class app/cash/redwood/treehouse/composeui/TreehouseContentKt { - public static final fun TreehouseContent (Lapp/cash/redwood/treehouse/TreehouseApp;Lapp/cash/redwood/treehouse/TreehouseView$WidgetSystem;Lapp/cash/redwood/treehouse/TreehouseContentSource;Landroidx/compose/ui/Modifier;Lapp/cash/redwood/treehouse/DynamicContentWidgetFactory;Landroidx/compose/runtime/Composer;II)V + public static final fun TreehouseContent (Lapp/cash/redwood/treehouse/TreehouseApp;Lapp/cash/redwood/treehouse/TreehouseView$WidgetSystem;Lapp/cash/redwood/treehouse/TreehouseContentSource;Landroidx/compose/ui/Modifier;Lapp/cash/redwood/ui/Margin;Lapp/cash/redwood/treehouse/DynamicContentWidgetFactory;Landroidx/compose/runtime/Composer;II)V } diff --git a/redwood-treehouse-host-composeui/api/redwood-treehouse-host-composeui.klib.api b/redwood-treehouse-host-composeui/api/redwood-treehouse-host-composeui.klib.api index 5daf91a9f9..910cdf7373 100644 --- a/redwood-treehouse-host-composeui/api/redwood-treehouse-host-composeui.klib.api +++ b/redwood-treehouse-host-composeui/api/redwood-treehouse-host-composeui.klib.api @@ -10,7 +10,7 @@ final val app.cash.redwood.treehouse.composeui/app_cash_redwood_treehouse_compos final val app.cash.redwood.treehouse.composeui/app_cash_redwood_treehouse_composeui_EmptyDynamicContentWidgetFactory_EmptyCrashed$stableprop // app.cash.redwood.treehouse.composeui/app_cash_redwood_treehouse_composeui_EmptyDynamicContentWidgetFactory_EmptyCrashed$stableprop|#static{}app_cash_redwood_treehouse_composeui_EmptyDynamicContentWidgetFactory_EmptyCrashed$stableprop[0] final val app.cash.redwood.treehouse.composeui/app_cash_redwood_treehouse_composeui_EmptyDynamicContentWidgetFactory_EmptyLoading$stableprop // app.cash.redwood.treehouse.composeui/app_cash_redwood_treehouse_composeui_EmptyDynamicContentWidgetFactory_EmptyLoading$stableprop|#static{}app_cash_redwood_treehouse_composeui_EmptyDynamicContentWidgetFactory_EmptyLoading$stableprop[0] -final fun <#A: app.cash.redwood.treehouse/AppService> app.cash.redwood.treehouse.composeui/TreehouseContent(app.cash.redwood.treehouse/TreehouseApp<#A>, app.cash.redwood.treehouse/TreehouseView.WidgetSystem>, app.cash.redwood.treehouse/TreehouseContentSource<#A>, androidx.compose.ui/Modifier?, app.cash.redwood.treehouse/DynamicContentWidgetFactory>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // app.cash.redwood.treehouse.composeui/TreehouseContent|TreehouseContent(app.cash.redwood.treehouse.TreehouseApp<0:0>;app.cash.redwood.treehouse.TreehouseView.WidgetSystem>;app.cash.redwood.treehouse.TreehouseContentSource<0:0>;androidx.compose.ui.Modifier?;app.cash.redwood.treehouse.DynamicContentWidgetFactory>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){0§}[0] +final fun <#A: app.cash.redwood.treehouse/AppService> app.cash.redwood.treehouse.composeui/TreehouseContent(app.cash.redwood.treehouse/TreehouseApp<#A>, app.cash.redwood.treehouse/TreehouseView.WidgetSystem>, app.cash.redwood.treehouse/TreehouseContentSource<#A>, androidx.compose.ui/Modifier?, app.cash.redwood.ui/Margin?, app.cash.redwood.treehouse/DynamicContentWidgetFactory>?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // app.cash.redwood.treehouse.composeui/TreehouseContent|TreehouseContent(app.cash.redwood.treehouse.TreehouseApp<0:0>;app.cash.redwood.treehouse.TreehouseView.WidgetSystem>;app.cash.redwood.treehouse.TreehouseContentSource<0:0>;androidx.compose.ui.Modifier?;app.cash.redwood.ui.Margin?;app.cash.redwood.treehouse.DynamicContentWidgetFactory>?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){0§}[0] final fun app.cash.redwood.treehouse.composeui/app_cash_redwood_treehouse_composeui_EmptyDynamicContentWidgetFactory$stableprop_getter(): kotlin/Int // app.cash.redwood.treehouse.composeui/app_cash_redwood_treehouse_composeui_EmptyDynamicContentWidgetFactory$stableprop_getter|app_cash_redwood_treehouse_composeui_EmptyDynamicContentWidgetFactory$stableprop_getter(){}[0] final fun app.cash.redwood.treehouse.composeui/app_cash_redwood_treehouse_composeui_EmptyDynamicContentWidgetFactory_EmptyCrashed$stableprop_getter(): kotlin/Int // app.cash.redwood.treehouse.composeui/app_cash_redwood_treehouse_composeui_EmptyDynamicContentWidgetFactory_EmptyCrashed$stableprop_getter|app_cash_redwood_treehouse_composeui_EmptyDynamicContentWidgetFactory_EmptyCrashed$stableprop_getter(){}[0] final fun app.cash.redwood.treehouse.composeui/app_cash_redwood_treehouse_composeui_EmptyDynamicContentWidgetFactory_EmptyLoading$stableprop_getter(): kotlin/Int // app.cash.redwood.treehouse.composeui/app_cash_redwood_treehouse_composeui_EmptyDynamicContentWidgetFactory_EmptyLoading$stableprop_getter|app_cash_redwood_treehouse_composeui_EmptyDynamicContentWidgetFactory_EmptyLoading$stableprop_getter(){}[0] 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..4e936414dc 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 @@ -55,6 +56,7 @@ public fun TreehouseContent( widgetSystem: WidgetSystem<@Composable () -> Unit>, contentSource: TreehouseContentSource, modifier: Modifier = Modifier, + windowInsets: Margin = Margin.Zero, dynamicContentWidgetFactory: DynamicContentWidgetFactory<@Composable () -> Unit> = EmptyDynamicContentWidgetFactory, ) { @@ -65,6 +67,7 @@ public fun TreehouseContent( val uiConfiguration = UiConfiguration( darkMode = isSystemInDarkTheme(), safeAreaInsets = safeAreaInsets(), + windowInsets = windowInsets, viewportSize = viewportSize, density = density.density.toDouble(), layoutDirection = when (LocalLayoutDirection.current) { @@ -83,6 +86,7 @@ public fun TreehouseContent( override val dynamicContentWidgetFactory = dynamicContentWidgetFactory override val onBackPressedDispatcher = onBackPressedDispatcher override val uiConfiguration = MutableStateFlow(uiConfiguration) + override var windowInsets: Margin = windowInsets // TODO TreehouseView is a weird type and shouldn't extend from RedwoodView. The concept // of this registry shouldn't exist for Treehouse / should be auto-wired via RedwoodContent. diff --git a/redwood-widget/api/android/redwood-widget.api b/redwood-widget/api/android/redwood-widget.api index 94cd94c664..55a86ac94b 100644 --- a/redwood-widget/api/android/redwood-widget.api +++ b/redwood-widget/api/android/redwood-widget.api @@ -50,15 +50,19 @@ public final class app/cash/redwood/widget/MutableListChildren : app/cash/redwoo public class app/cash/redwood/widget/RedwoodLayout : android/view/ViewGroup, app/cash/redwood/widget/RedwoodView { public fun (Landroid/content/Context;Landroidx/activity/OnBackPressedDispatcher;)V + public final fun consumeWindowInsets (I)V + public static synthetic fun consumeWindowInsets$default (Lapp/cash/redwood/widget/RedwoodLayout;IILjava/lang/Object;)V public final fun getChildren ()Lapp/cash/redwood/widget/Widget$Children; public fun getOnBackPressedDispatcher ()Lapp/cash/redwood/ui/OnBackPressedDispatcher; public fun getSavedStateRegistry ()Lapp/cash/redwood/widget/SavedStateRegistry; public fun getUiConfiguration ()Lkotlinx/coroutines/flow/StateFlow; public final fun getValue ()Landroid/view/View; public synthetic fun getValue ()Ljava/lang/Object; + public fun getWindowInsets ()Lapp/cash/redwood/ui/Margin; protected fun onConfigurationChanged (Landroid/content/res/Configuration;)V protected fun onLayout (ZIIII)V protected fun onMeasure (II)V + public fun setWindowInsets (Lapp/cash/redwood/ui/Margin;)V } public abstract interface class app/cash/redwood/widget/RedwoodView { @@ -67,6 +71,8 @@ public abstract interface class app/cash/redwood/widget/RedwoodView { public abstract fun getSavedStateRegistry ()Lapp/cash/redwood/widget/SavedStateRegistry; public abstract fun getUiConfiguration ()Lkotlinx/coroutines/flow/StateFlow; public abstract fun getValue ()Ljava/lang/Object; + public abstract fun getWindowInsets ()Lapp/cash/redwood/ui/Margin; + public abstract fun setWindowInsets (Lapp/cash/redwood/ui/Margin;)V } public abstract interface class app/cash/redwood/widget/SavedStateRegistry { diff --git a/redwood-widget/api/jvm/redwood-widget.api b/redwood-widget/api/jvm/redwood-widget.api index 35bf2dd73e..1b035c0312 100644 --- a/redwood-widget/api/jvm/redwood-widget.api +++ b/redwood-widget/api/jvm/redwood-widget.api @@ -54,6 +54,8 @@ public abstract interface class app/cash/redwood/widget/RedwoodView { public abstract fun getSavedStateRegistry ()Lapp/cash/redwood/widget/SavedStateRegistry; public abstract fun getUiConfiguration ()Lkotlinx/coroutines/flow/StateFlow; public abstract fun getValue ()Ljava/lang/Object; + public abstract fun getWindowInsets ()Lapp/cash/redwood/ui/Margin; + public abstract fun setWindowInsets (Lapp/cash/redwood/ui/Margin;)V } public abstract interface class app/cash/redwood/widget/SavedStateRegistry { diff --git a/redwood-widget/api/redwood-widget.klib.api b/redwood-widget/api/redwood-widget.klib.api index 542e594a7d..babdc1ad1f 100644 --- a/redwood-widget/api/redwood-widget.klib.api +++ b/redwood-widget/api/redwood-widget.klib.api @@ -18,6 +18,10 @@ abstract interface <#A: kotlin/Any> app.cash.redwood.widget/RedwoodView { // app abstract fun (): kotlinx.coroutines.flow/StateFlow // app.cash.redwood.widget/RedwoodView.uiConfiguration.|(){}[0] abstract val value // app.cash.redwood.widget/RedwoodView.value|{}value[0] abstract fun (): #A // app.cash.redwood.widget/RedwoodView.value.|(){}[0] + + abstract var windowInsets // app.cash.redwood.widget/RedwoodView.windowInsets|{}windowInsets[0] + abstract fun (): app.cash.redwood.ui/Margin // app.cash.redwood.widget/RedwoodView.windowInsets.|(){}[0] + abstract fun (app.cash.redwood.ui/Margin) // app.cash.redwood.widget/RedwoodView.windowInsets.|(app.cash.redwood.ui.Margin){}[0] } abstract interface <#A: kotlin/Any> app.cash.redwood.widget/Widget { // app.cash.redwood.widget/Widget|null[0] @@ -136,6 +140,10 @@ open class app.cash.redwood.widget/RedwoodUIView : app.cash.redwood.widget/Redwo open val value // app.cash.redwood.widget/RedwoodUIView.value|{}value[0] open fun (): platform.UIKit/UIView // app.cash.redwood.widget/RedwoodUIView.value.|(){}[0] + open var windowInsets // app.cash.redwood.widget/RedwoodUIView.windowInsets|{}windowInsets[0] + open fun (): app.cash.redwood.ui/Margin // app.cash.redwood.widget/RedwoodUIView.windowInsets.|(){}[0] + open fun (app.cash.redwood.ui/Margin) // app.cash.redwood.widget/RedwoodUIView.windowInsets.|(app.cash.redwood.ui.Margin){}[0] + open fun superviewChanged() // app.cash.redwood.widget/RedwoodUIView.superviewChanged|superviewChanged(){}[0] } 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..f953361a15 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 @@ -23,11 +23,14 @@ 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.ViewCompat +import androidx.core.view.WindowInsetsCompat 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 +54,11 @@ public open class RedwoodLayout( id = R.id.redwood_layout } - private val mutableUiConfiguration = MutableStateFlow(computeUiConfiguration()) + private val mutableUiConfiguration = MutableStateFlow( + computeUiConfiguration( + windowInsets = Margin.Zero, + ), + ) override val onBackPressedDispatcher: RedwoodOnBackPressedDispatcher = object : RedwoodOnBackPressedDispatcher { @@ -80,9 +87,15 @@ public open class RedwoodLayout( override val uiConfiguration: StateFlow get() = mutableUiConfiguration + override var windowInsets: Margin + get() = uiConfiguration.value.windowInsets + set(value) { + mutableUiConfiguration.value = uiConfiguration.value.copy(windowInsets = value) + } + init { setOnWindowInsetsChangeListener { insets -> - mutableUiConfiguration.value = computeUiConfiguration(insets = insets.safeDrawing) + mutableUiConfiguration.value = computeUiConfiguration(safeAreaInsets = insets.safeDrawing) } } @@ -112,7 +125,8 @@ public open class RedwoodLayout( private fun computeUiConfiguration( config: Configuration = context.resources.configuration, - insets: Insets = rootWindowInsetsCompat.safeDrawing, + safeAreaInsets: Insets = rootWindowInsetsCompat.safeDrawing, + windowInsets: Margin = this.windowInsets, ): UiConfiguration { val viewportSize: Size val density: Double @@ -122,7 +136,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 = safeAreaInsets.toMargin(Density(resources)), + windowInsets = windowInsets, viewportSize = viewportSize, density = density, layoutDirection = when (config.layoutDirection) { @@ -132,6 +147,22 @@ public open class RedwoodLayout( }, ) } + + /** + * Consume the identified insets from the enclosing UI in the Redwood composition. + * + * This installs a `OnApplyWindowInsetsListener` on this view that updates [windowInsets] each + * time the window insets are updated. + */ + public fun consumeWindowInsets( + mask: Int = (WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime()), + ) { + ViewCompat.setOnApplyWindowInsetsListener(this) { _, insets -> + val insetsForMask = insets.getInsets(mask) + windowInsets = insetsForMask.toMargin(Density(resources)) + WindowInsetsCompat.CONSUMED + } + } } private fun RedwoodOnBackPressedCallback.toAndroid(): AndroidOnBackPressedCallback = diff --git a/redwood-widget/src/commonMain/kotlin/app/cash/redwood/widget/RedwoodView.kt b/redwood-widget/src/commonMain/kotlin/app/cash/redwood/widget/RedwoodView.kt index b6f2e847cf..44a113d3b6 100644 --- a/redwood-widget/src/commonMain/kotlin/app/cash/redwood/widget/RedwoodView.kt +++ b/redwood-widget/src/commonMain/kotlin/app/cash/redwood/widget/RedwoodView.kt @@ -15,6 +15,7 @@ */ package app.cash.redwood.widget +import app.cash.redwood.ui.Margin import app.cash.redwood.ui.OnBackPressedDispatcher import app.cash.redwood.ui.UiConfiguration import kotlin.native.ObjCName @@ -28,6 +29,12 @@ public interface RedwoodView { public val value: W + /** + * The insets of the viewport that the composition is responsible for consuming. This value is + * passed to the composition as [UiConfiguration.windowInsets]. + */ + public var windowInsets: Margin + /** * The current dynamic content. If the application logic is stopped or crashed this will retain * a snapshot of the most-recent content, but the content will ignore user actions. 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..b2a4a442cd 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, + windowInsets = Margin.Zero, layoutDirection = valueRootView.effectiveUserInterfaceLayoutDirection, bounds = valueRootView.bounds, ), @@ -74,12 +75,20 @@ public open class RedwoodUIView : RedwoodView { override val uiConfiguration: StateFlow get() = mutableUiConfiguration + override var windowInsets: Margin + get() = uiConfiguration.value.windowInsets + set(value) { + mutableUiConfiguration.value = uiConfiguration.value.copy(windowInsets = value) + } + override val savedStateRegistry: SavedStateRegistry? get() = null private fun updateUiConfiguration() { + val old = mutableUiConfiguration.value mutableUiConfiguration.value = computeUiConfiguration( traitCollection = valueRootView.traitCollection, + windowInsets = old.windowInsets, layoutDirection = valueRootView.effectiveUserInterfaceLayoutDirection, bounds = valueRootView.bounds, ) @@ -124,12 +133,14 @@ public open class RedwoodUIView : RedwoodView { internal fun computeUiConfiguration( traitCollection: UITraitCollection, + windowInsets: Margin, layoutDirection: UIUserInterfaceLayoutDirection, bounds: CValue, ): UiConfiguration { return UiConfiguration( darkMode = traitCollection.userInterfaceStyle == UIUserInterfaceStyle.UIUserInterfaceStyleDark, safeAreaInsets = computeSafeAreaInsets(), + windowInsets = windowInsets, 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..6f04c36671 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 @@ -55,6 +56,12 @@ private class RedwoodHTMLElementView( private val _uiConfiguration: MutableStateFlow override val uiConfiguration: StateFlow get() = _uiConfiguration + override var windowInsets: Margin + get() = uiConfiguration.value.windowInsets + set(value) { + updateUiConfiguration { it.copy(windowInsets = value) } + } + override val savedStateRegistry: SavedStateRegistry? get() = null @@ -64,6 +71,7 @@ private class RedwoodHTMLElementView( _uiConfiguration = MutableStateFlow( UiConfiguration( darkMode = colorSchemeQuery.matches, + windowInsets = Margin.Zero, viewportSize = Size(width = value.offsetWidth.dp, height = value.offsetHeight.dp), layoutDirection = when (value.dir) { "ltr" -> LayoutDirection.Ltr @@ -75,15 +83,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 +107,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) {