diff --git a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/views/MayBeLoadingScreen.kt b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/views/MayBeLoadingScreen.kt index 81a300e16a..d16777d99b 100644 --- a/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/views/MayBeLoadingScreen.kt +++ b/benchmarks/performance-poetry/complex-poetry/src/main/java/com/squareup/benchmarks/performance/complex/poetry/views/MayBeLoadingScreen.kt @@ -3,21 +3,21 @@ package com.squareup.benchmarks.performance.complex.poetry.views import com.squareup.sample.container.overviewdetail.OverviewDetailScreen import com.squareup.sample.container.panel.ScrimScreen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.container.BodyAndModalsScreen -import com.squareup.workflow1.ui.container.ModalScreenOverlay +import com.squareup.workflow1.ui.container.BodyAndOverlaysScreen +import com.squareup.workflow1.ui.container.FullScreenOverlay @OptIn(WorkflowUiExperimentalApi::class) typealias MayBeLoadingScreen = - BodyAndModalsScreen, ModalScreenOverlay> + BodyAndOverlaysScreen, FullScreenOverlay> @OptIn(WorkflowUiExperimentalApi::class) fun MayBeLoadingScreen( baseScreen: OverviewDetailScreen, loaders: List = emptyList() ): MayBeLoadingScreen { - return BodyAndModalsScreen( + return BodyAndOverlaysScreen( ScrimScreen(baseScreen, dimmed = loaders.isNotEmpty()), - loaders.map { ModalScreenOverlay(it) } + loaders.map { FullScreenOverlay(it) } ) } @@ -27,4 +27,4 @@ val MayBeLoadingScreen.baseScreen: OverviewDetailScreen @OptIn(WorkflowUiExperimentalApi::class) val MayBeLoadingScreen.loaders: List - get() = modals.map { it.content } + get() = overlays.map { it.content } diff --git a/samples/containers/android/src/main/java/com/squareup/sample/container/panel/PanelOverlayDialogFactory.kt b/samples/containers/android/src/main/java/com/squareup/sample/container/panel/PanelOverlayDialogFactory.kt index 8541e6706d..5aac15b566 100644 --- a/samples/containers/android/src/main/java/com/squareup/sample/container/panel/PanelOverlayDialogFactory.kt +++ b/samples/containers/android/src/main/java/com/squareup/sample/container/panel/PanelOverlayDialogFactory.kt @@ -5,14 +5,14 @@ import android.graphics.Rect import android.view.View import com.squareup.sample.container.R import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.container.ModalScreenOverlayDialogFactory +import com.squareup.workflow1.ui.container.ScreenOverlayDialogFactory import com.squareup.workflow1.ui.container.setModalContent /** * Android support for [PanelOverlay]. */ @OptIn(WorkflowUiExperimentalApi::class) -internal object PanelOverlayDialogFactory : ModalScreenOverlayDialogFactory>( +internal object PanelOverlayDialogFactory : ScreenOverlayDialogFactory>( type = PanelOverlay::class ) { /** diff --git a/samples/containers/common/src/main/java/com/squareup/sample/container/panel/PanelOverlay.kt b/samples/containers/common/src/main/java/com/squareup/sample/container/panel/PanelOverlay.kt index a883a956c6..b9368f559b 100644 --- a/samples/containers/common/src/main/java/com/squareup/sample/container/panel/PanelOverlay.kt +++ b/samples/containers/common/src/main/java/com/squareup/sample/container/panel/PanelOverlay.kt @@ -2,9 +2,10 @@ package com.squareup.sample.container.panel import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.container.ModalOverlay import com.squareup.workflow1.ui.container.ScreenOverlay @OptIn(WorkflowUiExperimentalApi::class) class PanelOverlay( override val content: T -) : ScreenOverlay +) : ScreenOverlay, ModalOverlay diff --git a/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/AreYouSureWorkflow.kt b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/AreYouSureWorkflow.kt index cddd3421ff..0bf418de74 100644 --- a/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/AreYouSureWorkflow.kt +++ b/samples/containers/hello-back-button/src/main/java/com/squareup/sample/hellobackbutton/AreYouSureWorkflow.kt @@ -16,7 +16,7 @@ import com.squareup.workflow1.ui.container.AlertOverlay.Button.POSITIVE import com.squareup.workflow1.ui.container.AlertOverlay.Event.ButtonClicked import com.squareup.workflow1.ui.container.AlertOverlay.Event.Canceled import com.squareup.workflow1.ui.container.BackButtonScreen -import com.squareup.workflow1.ui.container.BodyAndModalsScreen +import com.squareup.workflow1.ui.container.BodyAndOverlaysScreen import com.squareup.workflow1.ui.toParcelable import com.squareup.workflow1.ui.toSnapshot import kotlinx.parcelize.Parcelize @@ -27,7 +27,7 @@ import kotlinx.parcelize.Parcelize */ @OptIn(WorkflowUiExperimentalApi::class) object AreYouSureWorkflow : - StatefulWorkflow>() { + StatefulWorkflow>() { override fun initialState( props: Unit, snapshot: Snapshot? @@ -45,12 +45,12 @@ object AreYouSureWorkflow : renderProps: Unit, renderState: State, context: RenderContext - ): BodyAndModalsScreen<*, AlertOverlay> { + ): BodyAndOverlaysScreen<*, AlertOverlay> { val ableBakerCharlie = context.renderChild(HelloBackButtonWorkflow, Unit) { noAction() } return when (renderState) { Running -> { - BodyAndModalsScreen( + BodyAndOverlaysScreen( BackButtonScreen(ableBakerCharlie) { // While we always provide a back button handler, by default the view code // associated with BackButtonScreen ignores ours if the view created for the @@ -80,7 +80,7 @@ object AreYouSureWorkflow : } ) - BodyAndModalsScreen(ableBakerCharlie, alert) + BodyAndOverlaysScreen(ableBakerCharlie, alert) } } } diff --git a/samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/TicTacToeWorkflow.kt b/samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/TicTacToeWorkflow.kt index 0262fd752a..4db026a4cb 100644 --- a/samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/TicTacToeWorkflow.kt +++ b/samples/tictactoe/common/src/main/java/com/squareup/sample/mainworkflow/TicTacToeWorkflow.kt @@ -21,7 +21,7 @@ import com.squareup.workflow1.action import com.squareup.workflow1.renderChild import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.container.BackStackScreen -import com.squareup.workflow1.ui.container.BodyAndModalsScreen +import com.squareup.workflow1.ui.container.BodyAndOverlaysScreen /** * Application specific root [Workflow], and demonstration of workflow composition. @@ -45,7 +45,7 @@ import com.squareup.workflow1.ui.container.BodyAndModalsScreen class TicTacToeWorkflow( private val authWorkflow: AuthWorkflow, private val runGameWorkflow: RunGameWorkflow -) : StatefulWorkflow, *>>() { +) : StatefulWorkflow, *>>() { override fun initialState( props: Unit, @@ -57,8 +57,8 @@ class TicTacToeWorkflow( renderProps: Unit, renderState: MainState, context: RenderContext - ): BodyAndModalsScreen, *> { - val bodyAndModals: BodyAndModalsScreen<*, *> = when (renderState) { + ): BodyAndOverlaysScreen, *> { + val bodyAndOverlays: BodyAndOverlaysScreen<*, *> = when (renderState) { is Authenticating -> { val authBackStack = context.renderChild(authWorkflow) { handleAuthResult(it) } // We always show an empty GameScreen behind the PanelOverlay that @@ -68,14 +68,14 @@ class TicTacToeWorkflow( // cheat is probably the most realistic thing about this sample. val emptyGameScreen = GamePlayScreen() - BodyAndModalsScreen(emptyGameScreen, PanelOverlay(authBackStack)) + BodyAndOverlaysScreen(emptyGameScreen, PanelOverlay(authBackStack)) } is RunningGame -> { val gameRendering = context.renderChild(runGameWorkflow) { startAuth } if (gameRendering.namePrompt == null) { - BodyAndModalsScreen(gameRendering.gameScreen, gameRendering.alerts) + BodyAndOverlaysScreen(gameRendering.gameScreen, gameRendering.alerts) } else { // To prompt for player names, the child puts up a ScreenOverlay, which // we replace here with a tasteful PanelOverlay. @@ -95,14 +95,14 @@ class TicTacToeWorkflow( BackStackScreen(gameRendering.namePrompt.content) val allModals = listOf(PanelOverlay(fullBackStack)) + gameRendering.alerts - BodyAndModalsScreen(gameRendering.gameScreen, allModals) + BodyAndOverlaysScreen(gameRendering.gameScreen, allModals) } } } // Add the scrim. Dim it only if there is a panel showing. - val dim = bodyAndModals.modals.any { modal -> modal is PanelOverlay<*> } - return bodyAndModals.mapBody { body -> ScrimScreen(body, dimmed = dim) } + val dim = bodyAndOverlays.overlays.any { modal -> modal is PanelOverlay<*> } + return bodyAndOverlays.mapBody { body -> ScrimScreen(body, dimmed = dim) } } override fun snapshotState(state: MainState): Snapshot = state.toSnapshot() diff --git a/samples/tictactoe/common/src/test/java/com/squareup/sample/mainworkflow/TicTacToeWorkflowTest.kt b/samples/tictactoe/common/src/test/java/com/squareup/sample/mainworkflow/TicTacToeWorkflowTest.kt index 5c52aa83b9..aa161b8a14 100644 --- a/samples/tictactoe/common/src/test/java/com/squareup/sample/mainworkflow/TicTacToeWorkflowTest.kt +++ b/samples/tictactoe/common/src/test/java/com/squareup/sample/mainworkflow/TicTacToeWorkflowTest.kt @@ -18,7 +18,7 @@ import com.squareup.workflow1.testing.launchForTestingFromStartWith import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.container.BackStackScreen -import com.squareup.workflow1.ui.container.BodyAndModalsScreen +import com.squareup.workflow1.ui.container.BodyAndOverlaysScreen import org.junit.Test /** @@ -63,8 +63,8 @@ class TicTacToeWorkflowTest { private fun authScreen(wrapped: String = DEFAULT_AUTH) = BackStackScreen(S(wrapped)) - private val BodyAndModalsScreen, *>.panels: List> - get() = modals.mapNotNull { it as? PanelOverlay<*> } + private val BodyAndOverlaysScreen, *>.panels: List> + get() = overlays.mapNotNull { it as? PanelOverlay<*> } private fun authWorkflow( screen: String = DEFAULT_AUTH diff --git a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewTreeIntegrationTest.kt b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewTreeIntegrationTest.kt index 601f11ed25..6a0b8ec105 100644 --- a/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewTreeIntegrationTest.kt +++ b/workflow-ui/compose/src/androidTest/java/com/squareup/workflow1/ui/compose/ComposeViewTreeIntegrationTest.kt @@ -35,9 +35,9 @@ import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.WorkflowUiExperimentalApi import com.squareup.workflow1.ui.container.AndroidOverlay import com.squareup.workflow1.ui.container.BackStackScreen -import com.squareup.workflow1.ui.container.BodyAndModalsScreen -import com.squareup.workflow1.ui.container.ModalScreenOverlayDialogFactory +import com.squareup.workflow1.ui.container.BodyAndOverlaysScreen import com.squareup.workflow1.ui.container.ScreenOverlay +import com.squareup.workflow1.ui.container.ScreenOverlayDialogFactory import com.squareup.workflow1.ui.internal.test.DetectLeaksAfterTestSuccess import com.squareup.workflow1.ui.internal.test.IdleAfterTestRule import com.squareup.workflow1.ui.internal.test.IdlingDispatcherRule @@ -371,7 +371,7 @@ internal class ComposeViewTreeIntegrationTest { // Show first screen to initialize state. scenario.onActivity { it.setRendering( - BodyAndModalsScreen( + BodyAndOverlaysScreen( EmptyRendering, TestModal(BackStackScreen(EmptyRendering, firstScreen)) ) ) @@ -426,7 +426,7 @@ internal class ComposeViewTreeIntegrationTest { // Show first screen to initialize state. scenario.onActivity { it.setRendering( - BodyAndModalsScreen( + BodyAndOverlaysScreen( EmptyRendering, TestModal(firstScreen), TestModal(secondScreen), TestModal(thirdScreen) ) ) @@ -485,7 +485,7 @@ internal class ComposeViewTreeIntegrationTest { // Show first screen to initialize state. scenario.onActivity { it.setRendering( - BodyAndModalsScreen( + BodyAndOverlaysScreen( EmptyRendering, TestModal(BackStackScreen(EmptyRendering, layer0Screen0)), TestModal(BackStackScreen(EmptyRendering, layer1Screen0)), @@ -508,7 +508,7 @@ internal class ComposeViewTreeIntegrationTest { // Push some screens onto the backstack. scenario.onActivity { it.setRendering( - BodyAndModalsScreen( + BodyAndOverlaysScreen( EmptyRendering, TestModal(BackStackScreen(EmptyRendering, layer0Screen0, layer0Screen1)), TestModal(BackStackScreen(EmptyRendering, layer1Screen0, layer1Screen1)), @@ -545,7 +545,7 @@ internal class ComposeViewTreeIntegrationTest { // Pop both backstacks and check that screens were restored. scenario.onActivity { it.setRendering( - BodyAndModalsScreen( + BodyAndOverlaysScreen( EmptyRendering, TestModal(BackStackScreen(EmptyRendering, layer0Screen0)), TestModal(BackStackScreen(EmptyRendering, layer1Screen0)), @@ -566,7 +566,7 @@ internal class ComposeViewTreeIntegrationTest { data class TestModal( override val content: Screen ) : ScreenOverlay, AndroidOverlay { - override val dialogFactory = object : ModalScreenOverlayDialogFactory( + override val dialogFactory = object : ScreenOverlayDialogFactory( TestModal::class ) { override fun buildDialogWithContentView(contentView: View): Dialog { diff --git a/workflow-ui/container-common/src/main/java/com/squareup/workflow1/ui/modal/AlertContainerScreen.kt b/workflow-ui/container-common/src/main/java/com/squareup/workflow1/ui/modal/AlertContainerScreen.kt index 4e2f717d9b..ed26c00178 100644 --- a/workflow-ui/container-common/src/main/java/com/squareup/workflow1/ui/modal/AlertContainerScreen.kt +++ b/workflow-ui/container-common/src/main/java/com/squareup/workflow1/ui/modal/AlertContainerScreen.kt @@ -7,7 +7,7 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi /** * **This will be deprecated in favor of * [AlertOverlay][com.squareup.workflow1.ui.container.AlertOverlay] and - * [BodyAndModalsScreen][com.squareup.workflow1.ui.container.BodyAndModalsScreen] + * [BodyAndModalsScreen][com.squareup.workflow1.ui.container.BodyAndOverlaysScreen] * very soon.** * * May show a stack of [AlertScreen] over a [beneathModals]. diff --git a/workflow-ui/container-common/src/main/java/com/squareup/workflow1/ui/modal/HasModals.kt b/workflow-ui/container-common/src/main/java/com/squareup/workflow1/ui/modal/HasModals.kt index 57dcf1590c..4bf4deae83 100644 --- a/workflow-ui/container-common/src/main/java/com/squareup/workflow1/ui/modal/HasModals.kt +++ b/workflow-ui/container-common/src/main/java/com/squareup/workflow1/ui/modal/HasModals.kt @@ -4,7 +4,7 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi /** * **This will be deprecated in favor of - * [BodyAndModalsScreen][com.squareup.workflow1.ui.container.BodyAndModalsScreen] + * [BodyAndModalsScreen][com.squareup.workflow1.ui.container.BodyAndOverlaysScreen] * very soon.** * * Interface implemented by screen classes that represent a stack of diff --git a/workflow-ui/core-android/api/core-android.api b/workflow-ui/core-android/api/core-android.api index 85188f9988..34d5f60f45 100644 --- a/workflow-ui/core-android/api/core-android.api +++ b/workflow-ui/core-android/api/core-android.api @@ -511,8 +511,8 @@ public final class com/squareup/workflow1/ui/container/BackStackScreenViewFactor public fun getType ()Lkotlin/reflect/KClass; } -public final class com/squareup/workflow1/ui/container/BodyAndModalsContainer : android/widget/FrameLayout { - public static final field Companion Lcom/squareup/workflow1/ui/container/BodyAndModalsContainer$Companion; +public final class com/squareup/workflow1/ui/container/BodyAndOverlaysContainer : android/widget/FrameLayout { + public static final field Companion Lcom/squareup/workflow1/ui/container/BodyAndOverlaysContainer$Companion; public fun (Landroid/content/Context;)V public fun (Landroid/content/Context;Landroid/util/AttributeSet;)V public fun (Landroid/content/Context;Landroid/util/AttributeSet;I)V @@ -520,12 +520,12 @@ public final class com/squareup/workflow1/ui/container/BodyAndModalsContainer : public synthetic fun (Landroid/content/Context;Landroid/util/AttributeSet;IIILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun dispatchKeyEvent (Landroid/view/KeyEvent;)Z public fun dispatchTouchEvent (Landroid/view/MotionEvent;)Z - public final fun update (Lcom/squareup/workflow1/ui/container/BodyAndModalsScreen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V + public final fun update (Lcom/squareup/workflow1/ui/container/BodyAndOverlaysScreen;Lcom/squareup/workflow1/ui/ViewEnvironment;)V } -public final class com/squareup/workflow1/ui/container/BodyAndModalsContainer$Companion : com/squareup/workflow1/ui/ScreenViewFactory { +public final class com/squareup/workflow1/ui/container/BodyAndOverlaysContainer$Companion : com/squareup/workflow1/ui/ScreenViewFactory { public synthetic fun buildView (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Lcom/squareup/workflow1/ui/ScreenViewHolder; - public fun buildView (Lcom/squareup/workflow1/ui/container/BodyAndModalsScreen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Lcom/squareup/workflow1/ui/ScreenViewHolder; + public fun buildView (Lcom/squareup/workflow1/ui/container/BodyAndOverlaysScreen;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;Landroid/view/ViewGroup;)Lcom/squareup/workflow1/ui/ScreenViewHolder; public fun getType ()Lkotlin/reflect/KClass; } @@ -536,7 +536,7 @@ public final class com/squareup/workflow1/ui/container/CoveredByModal : com/squa } public final class com/squareup/workflow1/ui/container/DialogHolder { - public fun (Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;IZLandroid/content/Context;Lcom/squareup/workflow1/ui/container/OverlayDialogFactory;)V + public fun (Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;ILandroid/content/Context;Lcom/squareup/workflow1/ui/container/OverlayDialogFactory;)V public final fun canTakeRendering (Lcom/squareup/workflow1/ui/container/Overlay;)Z public final fun dismiss ()V public final fun getEnvironment ()Lcom/squareup/workflow1/ui/ViewEnvironment; @@ -587,14 +587,18 @@ public final class com/squareup/workflow1/ui/container/EnvironmentScreenViewFact } public final class com/squareup/workflow1/ui/container/LayeredDialogs { - public fun (Landroid/content/Context;ZLkotlin/jvm/functions/Function0;)V - public fun (Landroid/view/View;Z)V - public final fun attachToParentRegistryOwner (Ljava/lang/String;Landroidx/savedstate/SavedStateRegistryOwner;)V - public final fun detachFromParentRegistry ()V - public final fun getHasDialogs ()Z + public static final field Companion Lcom/squareup/workflow1/ui/container/LayeredDialogs$Companion; + public synthetic fun (Landroid/content/Context;Lkotlinx/coroutines/flow/StateFlow;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getAllowEvents ()Z + public final fun onAttachedToWindow (Ljava/lang/String;Landroid/view/View;)V + public final fun onDetachedFromWindow ()V public final fun onRestoreInstanceState (Lcom/squareup/workflow1/ui/container/LayeredDialogs$SavedState;)V public final fun onSaveInstanceState ()Lcom/squareup/workflow1/ui/container/LayeredDialogs$SavedState; - public final fun update (Ljava/util/List;Lcom/squareup/workflow1/ui/ViewEnvironment;)V + public final fun update (Ljava/util/List;Lcom/squareup/workflow1/ui/ViewEnvironment;Lkotlin/jvm/functions/Function1;)V +} + +public final class com/squareup/workflow1/ui/container/LayeredDialogs$Companion { + public final fun forView (Landroid/view/View;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/container/LayeredDialogs; } public final class com/squareup/workflow1/ui/container/LayeredDialogs$SavedState : android/os/Parcelable { @@ -613,36 +617,6 @@ public final class com/squareup/workflow1/ui/container/LayeredDialogs$SavedState public synthetic fun newArray (I)[Ljava/lang/Object; } -public final class com/squareup/workflow1/ui/container/ModalArea { - public static final field Companion Lcom/squareup/workflow1/ui/container/ModalArea$Companion; - public fun (Lkotlinx/coroutines/flow/StateFlow;)V - public final fun getBounds ()Lkotlinx/coroutines/flow/StateFlow; -} - -public final class com/squareup/workflow1/ui/container/ModalArea$Companion : com/squareup/workflow1/ui/ViewEnvironmentKey { - public fun getDefault ()Lcom/squareup/workflow1/ui/container/ModalArea; - public synthetic fun getDefault ()Ljava/lang/Object; -} - -public final class com/squareup/workflow1/ui/container/ModalAreaKt { - public static final fun plus (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/container/ModalArea;)Lcom/squareup/workflow1/ui/ViewEnvironment; -} - -public class com/squareup/workflow1/ui/container/ModalScreenOverlayDialogFactory : com/squareup/workflow1/ui/container/OverlayDialogFactory { - public fun (Lkotlin/reflect/KClass;)V - public synthetic fun buildDialog (Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Landroid/app/Dialog; - public final fun buildDialog (Lcom/squareup/workflow1/ui/container/ScreenOverlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Landroid/app/Dialog; - public fun buildDialogWithContentView (Landroid/view/View;)Landroid/app/Dialog; - public fun getType ()Lkotlin/reflect/KClass; - public fun updateBounds (Landroid/app/Dialog;Landroid/graphics/Rect;)V - public synthetic fun updateDialog (Landroid/app/Dialog;Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;)V - public final fun updateDialog (Landroid/app/Dialog;Lcom/squareup/workflow1/ui/container/ScreenOverlay;Lcom/squareup/workflow1/ui/ViewEnvironment;)V -} - -public final class com/squareup/workflow1/ui/container/ModalScreenOverlayDialogFactoryKt { - public static final fun setModalContent (Landroid/app/Dialog;Landroid/view/View;)V -} - public abstract interface class com/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed { public static final field Companion Lcom/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed$Companion; public abstract fun onBackPressed (Landroid/view/View;)Z @@ -657,6 +631,21 @@ public final class com/squareup/workflow1/ui/container/ModalScreenOverlayOnBackP public static final fun plus (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed;)Lcom/squareup/workflow1/ui/ViewEnvironment; } +public final class com/squareup/workflow1/ui/container/OverlayArea { + public static final field Companion Lcom/squareup/workflow1/ui/container/OverlayArea$Companion; + public fun (Lkotlinx/coroutines/flow/StateFlow;)V + public final fun getBounds ()Lkotlinx/coroutines/flow/StateFlow; +} + +public final class com/squareup/workflow1/ui/container/OverlayArea$Companion : com/squareup/workflow1/ui/ViewEnvironmentKey { + public fun getDefault ()Lcom/squareup/workflow1/ui/container/OverlayArea; + public synthetic fun getDefault ()Ljava/lang/Object; +} + +public final class com/squareup/workflow1/ui/container/OverlayAreaKt { + public static final fun plus (Lcom/squareup/workflow1/ui/ViewEnvironment;Lcom/squareup/workflow1/ui/container/OverlayArea;)Lcom/squareup/workflow1/ui/ViewEnvironment; +} + public abstract interface class com/squareup/workflow1/ui/container/OverlayDialogFactory : com/squareup/workflow1/ui/ViewRegistry$Entry { public abstract fun buildDialog (Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Landroid/app/Dialog; public abstract fun updateDialog (Landroid/app/Dialog;Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;)V @@ -680,6 +669,21 @@ public final class com/squareup/workflow1/ui/container/OverlayDialogFactoryKt { public static final fun toDialogFactory (Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;)Lcom/squareup/workflow1/ui/container/OverlayDialogFactory; } +public class com/squareup/workflow1/ui/container/ScreenOverlayDialogFactory : com/squareup/workflow1/ui/container/OverlayDialogFactory { + public fun (Lkotlin/reflect/KClass;)V + public synthetic fun buildDialog (Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Landroid/app/Dialog; + public final fun buildDialog (Lcom/squareup/workflow1/ui/container/ScreenOverlay;Lcom/squareup/workflow1/ui/ViewEnvironment;Landroid/content/Context;)Landroid/app/Dialog; + public fun buildDialogWithContentView (Landroid/view/View;)Landroid/app/Dialog; + public fun getType ()Lkotlin/reflect/KClass; + public fun updateBounds (Landroid/app/Dialog;Landroid/graphics/Rect;)V + public synthetic fun updateDialog (Landroid/app/Dialog;Lcom/squareup/workflow1/ui/container/Overlay;Lcom/squareup/workflow1/ui/ViewEnvironment;)V + public final fun updateDialog (Landroid/app/Dialog;Lcom/squareup/workflow1/ui/container/ScreenOverlay;Lcom/squareup/workflow1/ui/ViewEnvironment;)V +} + +public final class com/squareup/workflow1/ui/container/ScreenOverlayDialogFactoryKt { + public static final fun setModalContent (Landroid/app/Dialog;Landroid/view/View;)V +} + public final class com/squareup/workflow1/ui/container/ViewStateCache { public fun ()V public fun (Ljava/util/Map;)V diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactoryFinder.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactoryFinder.kt index 90e216a2a8..3b8e4cf976 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactoryFinder.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/ScreenViewFactoryFinder.kt @@ -3,8 +3,8 @@ package com.squareup.workflow1.ui import com.squareup.workflow1.ui.ScreenViewFactory.Companion.forWrapper import com.squareup.workflow1.ui.container.BackStackScreen import com.squareup.workflow1.ui.container.BackStackScreenViewFactory -import com.squareup.workflow1.ui.container.BodyAndModalsContainer -import com.squareup.workflow1.ui.container.BodyAndModalsScreen +import com.squareup.workflow1.ui.container.BodyAndOverlaysContainer +import com.squareup.workflow1.ui.container.BodyAndOverlaysScreen import com.squareup.workflow1.ui.container.EnvironmentScreen import com.squareup.workflow1.ui.container.EnvironmentScreenViewFactory @@ -67,8 +67,8 @@ public interface ScreenViewFactoryFinder { ?: (rendering as? BackStackScreen<*>)?.let { BackStackScreenViewFactory as ScreenViewFactory } - ?: (rendering as? BodyAndModalsScreen<*, *>)?.let { - BodyAndModalsContainer as ScreenViewFactory + ?: (rendering as? BodyAndOverlaysScreen<*, *>)?.let { + BodyAndOverlaysContainer as ScreenViewFactory } ?: (rendering as? NamedScreen<*>)?.let { forWrapper, ScreenT> { it.wrapped } as ScreenViewFactory diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AndroidDialogBounds.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AndroidDialogBounds.kt index 6013d72900..68852d50a4 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AndroidDialogBounds.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/AndroidDialogBounds.kt @@ -7,7 +7,6 @@ import android.view.View import android.view.Window import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.environment import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel @@ -20,7 +19,7 @@ import kotlinx.coroutines.flow.onEach * [bounds] is expected to be in global display coordinates, * e.g. as returned from [View.getGlobalVisibleRect]. * - * @see ModalScreenOverlayDialogFactory.updateBounds + * @see ScreenOverlayDialogFactory.updateBounds */ @WorkflowUiExperimentalApi public fun Dialog.setBounds(bounds: Rect) { @@ -40,7 +39,7 @@ internal fun D.maintainBounds( environment: ViewEnvironment, onBoundsChange: (D, Rect) -> Unit ) { - maintainBounds(environment[ModalArea].bounds, onBoundsChange) + maintainBounds(environment[OverlayArea].bounds, onBoundsChange) } @WorkflowUiExperimentalApi diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndModalsContainer.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndModalsContainer.kt deleted file mode 100644 index 9fdc00c010..0000000000 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndModalsContainer.kt +++ /dev/null @@ -1,196 +0,0 @@ -package com.squareup.workflow1.ui.container - -import android.content.Context -import android.graphics.Rect -import android.os.Parcel -import android.os.Parcelable -import android.os.Parcelable.Creator -import android.util.AttributeSet -import android.view.KeyEvent -import android.view.MotionEvent -import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import android.view.ViewTreeObserver.OnGlobalLayoutListener -import android.widget.FrameLayout -import com.squareup.workflow1.ui.Compatible -import com.squareup.workflow1.ui.Compatible.Companion.keyFor -import com.squareup.workflow1.ui.R -import com.squareup.workflow1.ui.ScreenViewFactory -import com.squareup.workflow1.ui.ScreenViewHolder -import com.squareup.workflow1.ui.ScreenViewHolder.Companion.Showing -import com.squareup.workflow1.ui.ViewEnvironment -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import com.squareup.workflow1.ui.WorkflowViewStub -import com.squareup.workflow1.ui.androidx.WorkflowAndroidXSupport -import kotlinx.coroutines.flow.MutableStateFlow - -@WorkflowUiExperimentalApi -internal class BodyAndModalsContainer @JvmOverloads constructor( - context: Context, - attributeSet: AttributeSet? = null, - defStyle: Int = 0, - defStyleRes: Int = 0 -) : FrameLayout(context, attributeSet, defStyle, defStyleRes) { - /** - * Unique identifier for this view for SavedStateRegistry purposes. Based on the - * [Compatible.keyFor] the current rendering. Taking this approach allows - * feature developers to take control over naming, e.g. by wrapping renderings - * with [NamedScreen][com.squareup.workflow1.ui.NamedScreen]. - */ - private lateinit var savedStateParentKey: String - - private val baseViewStub: WorkflowViewStub = WorkflowViewStub(context).also { - addView(it, ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)) - } - - private val dialogs = LayeredDialogs(view = this, modal = true) - - // The bounds of this view in global (display) coordinates, as reported - // by getGlobalVisibleRect. - // - // Made available to managed ModalScreenOverlayDialogFactory instances - // via the ModalArea key in ViewEnvironment. When this updates their - // updateBounds methods will fire. They should resize themselves to - // avoid covering peers of this view. - private val bounds = MutableStateFlow(Rect()) - private val boundsRect = Rect() - - private val boundsListener = OnGlobalLayoutListener { - if (getGlobalVisibleRect(boundsRect) && boundsRect != bounds.value) { - bounds.value = Rect(boundsRect) - } - // Should we close the dialogs if getGlobalVisibleRect returns false? - // https://github.com/square/workflow-kotlin/issues/599 - } - - // Note similar code in DialogHolder. - private var allowEvents = true - set(value) { - val was = field - field = value - if (value != was) { - // https://stackoverflow.com/questions/2886407/dealing-with-rapid-tapping-on-buttons - // If any motion events were enqueued on the main thread, cancel them. - dispatchCancelEvent { super.dispatchTouchEvent(it) } - // When we cancel, have to warn things like RecyclerView that handle streams - // of motion events and eventually dispatch input events (click, key pressed, etc.) - // based on them. - cancelPendingInputEvents() - } - } - - fun update( - newScreen: BodyAndModalsScreen<*, *>, - viewEnvironment: ViewEnvironment - ) { - savedStateParentKey = keyFor(viewEnvironment[Showing]) - - val showingModals = newScreen.modals.isNotEmpty() - - // There is a long wait from when we show a dialog until it starts blocking - // events for us. To compensate we ignore all touches while any dialogs exist. - allowEvents = !showingModals - - val baseEnv = if (showingModals) viewEnvironment + (CoveredByModal to true) else viewEnvironment - baseViewStub.show(newScreen.body, baseEnv) - - // Allow modal dialogs to restrict themselves to cover only this view. - val dialogsEnv = if (showingModals) viewEnvironment + ModalArea(bounds) else viewEnvironment - - dialogs.update(newScreen.modals, dialogsEnv) - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - boundsListener.onGlobalLayout() - viewTreeObserver.addOnGlobalLayoutListener(boundsListener) - - // Wire up dialogs to our parent SavedStateRegistry. - val parentRegistryOwner = WorkflowAndroidXSupport.stateRegistryOwnerFromViewTreeOrContext(this) - dialogs.attachToParentRegistryOwner(savedStateParentKey, parentRegistryOwner) - } - - override fun onDetachedFromWindow() { - // Disconnect dialogs from our parent SavedStateRegistry so that it doesn't get asked - // to save state anymore. - dialogs.detachFromParentRegistry() - // Don't leak the dialogs if we're suddenly yanked out of view. - // https://github.com/square/workflow-kotlin/issues/314 - dialogs.update(emptyList(), ViewEnvironment.EMPTY) - viewTreeObserver.removeOnGlobalLayoutListener(boundsListener) - bounds.value = Rect() - super.onDetachedFromWindow() - } - - override fun dispatchTouchEvent(event: MotionEvent): Boolean { - return !allowEvents || super.dispatchTouchEvent(event) - } - - override fun dispatchKeyEvent(event: KeyEvent): Boolean { - return !allowEvents || super.dispatchKeyEvent(event) - } - - override fun onSaveInstanceState(): Parcelable { - return SavedState( - superState = super.onSaveInstanceState()!!, - savedDialogs = dialogs.onSaveInstanceState() - ) - } - - override fun onRestoreInstanceState(state: Parcelable) { - (state as? SavedState) - ?.let { - dialogs.onRestoreInstanceState(state.savedDialogs) - super.onRestoreInstanceState(state.superState) - } - ?: super.onRestoreInstanceState(super.onSaveInstanceState()) - // Some other class wrote state, but we're not allowed to skip - // the call to super. Make a no-op call. - } - - private class SavedState : BaseSavedState { - constructor( - superState: Parcelable, - savedDialogs: LayeredDialogs.SavedState - ) : super(superState) { - this.savedDialogs = savedDialogs - } - - constructor(source: Parcel) : super(source) { - @Suppress("UNCHECKED_CAST") - savedDialogs = source.readParcelable(SavedState::class.java.classLoader)!! - } - - val savedDialogs: LayeredDialogs.SavedState - - override fun writeToParcel( - out: Parcel, - flags: Int - ) { - super.writeToParcel(out, flags) - out.writeParcelable(savedDialogs, flags) - } - - companion object CREATOR : Creator { - override fun createFromParcel(source: Parcel): SavedState = - SavedState(source) - - override fun newArray(size: Int): Array = arrayOfNulls(size) - } - } - - companion object : ScreenViewFactory> - by ScreenViewFactory.fromCode( - buildView = { _, initialEnvironment, context, _ -> - BodyAndModalsContainer(context) - .let { view -> - view.id = R.id.workflow_body_and_modals_container - view.layoutParams = (LayoutParams(MATCH_PARENT, MATCH_PARENT)) - - ScreenViewHolder(initialEnvironment, view) { rendering, environment -> - view.update(rendering, environment) - } - } - } - ) -} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndOverlaysContainer.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndOverlaysContainer.kt new file mode 100644 index 0000000000..1e8a94845b --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/BodyAndOverlaysContainer.kt @@ -0,0 +1,145 @@ +package com.squareup.workflow1.ui.container + +import android.content.Context +import android.os.Parcel +import android.os.Parcelable +import android.os.Parcelable.Creator +import android.util.AttributeSet +import android.view.KeyEvent +import android.view.MotionEvent +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.widget.FrameLayout +import com.squareup.workflow1.ui.Compatible +import com.squareup.workflow1.ui.R +import com.squareup.workflow1.ui.ScreenViewFactory +import com.squareup.workflow1.ui.ScreenViewHolder +import com.squareup.workflow1.ui.ScreenViewHolder.Companion.Showing +import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.WorkflowViewStub + +@WorkflowUiExperimentalApi +internal class BodyAndOverlaysContainer @JvmOverloads constructor( + context: Context, + attributeSet: AttributeSet? = null, + defStyle: Int = 0, + defStyleRes: Int = 0 +) : FrameLayout(context, attributeSet, defStyle, defStyleRes) { + /** + * The unique `SavedStateRegistry` key passed to [LayeredDialogs.onAttachedToWindow], + * derived from the first rendering passed to [update]. See the doc on + * [LayeredDialogs.onAttachedToWindow] for details. + */ + private lateinit var savedStateParentKey: String + + private val baseViewStub: WorkflowViewStub = WorkflowViewStub(context).also { + addView(it, ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)) + } + + private val dialogs = LayeredDialogs.forView( + view = this, + superDispatchTouchEvent = { super.dispatchTouchEvent(it) } + ) + + fun update( + newScreen: BodyAndOverlaysScreen<*, *>, + viewEnvironment: ViewEnvironment + ) { + savedStateParentKey = Compatible.keyFor(viewEnvironment[Showing]) + + dialogs.update(newScreen.overlays, viewEnvironment) { env -> + baseViewStub.show(newScreen.body, env) + } + } + + override fun onAttachedToWindow() { + // I tried to move this to the attachStateChangeListener in LayeredDialogs.Companion.forView, + // but that fires too late and we crash with the dreaded + // "You can consumeRestoredStateForKey only after super.onCreate of corresponding component". + + super.onAttachedToWindow() + // Wire up dialogs to our parent SavedStateRegistry. + dialogs.onAttachedToWindow(savedStateParentKey, this) + } + + override fun onDetachedFromWindow() { + // Disconnect dialogs from our parent SavedStateRegistry so that it doesn't get asked + // to save state anymore. + dialogs.onDetachedFromWindow() + + super.onDetachedFromWindow() + } + + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + return !dialogs.allowEvents || super.dispatchTouchEvent(event) + } + + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + return !dialogs.allowEvents || super.dispatchKeyEvent(event) + } + + override fun onSaveInstanceState(): Parcelable { + return SavedState( + superState = super.onSaveInstanceState()!!, + savedDialogs = dialogs.onSaveInstanceState() + ) + } + + override fun onRestoreInstanceState(state: Parcelable) { + (state as? SavedState) + ?.let { + dialogs.onRestoreInstanceState(state.savedDialogs) + super.onRestoreInstanceState(state.superState) + } + ?: super.onRestoreInstanceState(super.onSaveInstanceState()) + // Some other class wrote state, but we're not allowed to skip + // the call to super. Make a no-op call. + } + + private class SavedState : BaseSavedState { + constructor( + superState: Parcelable, + savedDialogs: LayeredDialogs.SavedState + ) : super(superState) { + this.savedDialogs = savedDialogs + } + + constructor(source: Parcel) : super(source) { + @Suppress("UNCHECKED_CAST") + savedDialogs = source.readParcelable(SavedState::class.java.classLoader)!! + } + + val savedDialogs: LayeredDialogs.SavedState + + override fun writeToParcel( + out: Parcel, + flags: Int + ) { + super.writeToParcel(out, flags) + out.writeParcelable(savedDialogs, flags) + } + + companion object CREATOR : Creator { + override fun createFromParcel(source: Parcel): SavedState = + SavedState(source) + + override fun newArray(size: Int): Array = arrayOfNulls(size) + } + } + + companion object : ScreenViewFactory> + by ScreenViewFactory.fromCode( + buildView = { _, initialEnvironment, context, _ -> + BodyAndOverlaysContainer(context) + .let { view -> + view.id = R.id.workflow_body_and_modals_container + view.layoutParams = (LayoutParams(MATCH_PARENT, MATCH_PARENT)) + + ScreenViewHolder(initialEnvironment, view) { rendering, environment -> + view.update(rendering, environment) + } + } + } + ) +} diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/CoveredByModal.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/CoveredByModal.kt index db771dccc1..163d5779cc 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/CoveredByModal.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/CoveredByModal.kt @@ -4,8 +4,8 @@ import com.squareup.workflow1.ui.ViewEnvironmentKey import com.squareup.workflow1.ui.WorkflowUiExperimentalApi /** - * True in views managed by [BodyAndModalsScreen] when their events are being blocked - * by a modal [Overlay]. + * True in views managed by [BodyAndOverlaysScreen] when their events are being blocked + * by a [ModalOverlay]. */ @WorkflowUiExperimentalApi internal object CoveredByModal : ViewEnvironmentKey(type = Boolean::class) { diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/DialogHolder.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/DialogHolder.kt index cce198f90f..49ea7803eb 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/DialogHolder.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/DialogHolder.kt @@ -22,15 +22,12 @@ import com.squareup.workflow1.ui.compatible /** * Used by [LayeredDialogs] to keep a [Dialog] tied to its [rendering] and [environment]. - * - * @param modal if true, ignore keyboard and touch events when [CoveredByModal] is also true */ @WorkflowUiExperimentalApi internal class DialogHolder( initialRendering: T, initialViewEnvironment: ViewEnvironment, index: Int, - private val modal: Boolean, private val context: Context, private val factory: OverlayDialogFactory ) { @@ -42,9 +39,11 @@ internal class DialogHolder( var environment: ViewEnvironment = initialViewEnvironment private set + private val modal = initialRendering is ModalOverlay + private var dialogOrNull: Dialog? = null - // Note similar code in BodyAndModalsContainer + // Note similar code in LayeredDialogs private var allowEvents = true set(value) { val was = field diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/LayeredDialogs.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/LayeredDialogs.kt index 3b885d0c77..63ecb77e10 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/LayeredDialogs.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/LayeredDialogs.kt @@ -1,52 +1,51 @@ package com.squareup.workflow1.ui.container import android.content.Context +import android.graphics.Rect import android.os.Parcel import android.os.Parcelable import android.os.Parcelable.Creator +import android.view.MotionEvent import android.view.View +import android.view.View.OnAttachStateChangeListener +import android.view.ViewTreeObserver.OnGlobalLayoutListener import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewTreeLifecycleOwner -import androidx.savedstate.SavedStateRegistryOwner +import com.squareup.workflow1.ui.Compatible import com.squareup.workflow1.ui.ViewEnvironment import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import com.squareup.workflow1.ui.androidx.WorkflowAndroidXSupport import com.squareup.workflow1.ui.androidx.WorkflowLifecycleOwner import com.squareup.workflow1.ui.androidx.WorkflowSavedStateRegistryAggregator import com.squareup.workflow1.ui.container.DialogHolder.KeyAndBundle +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow /** * Does the bulk of the work of maintaining a set of [Dialog][android.app.Dialog]s * to reflect lists of [Overlay]. Can be used to create custom [Overlay]-based - * layouts if [BodyAndModalsScreen] or the default [View] bound to it are too restrictive. + * layouts if [BodyAndOverlaysScreen] or the default [View] bound to it are too restrictive. * Provides a [LifecycleOwner] per managed dialog, and view persistence support. * - * @param modal When true, only the top-most dialog is allowed to process touch and key events + * @param bounds made available to managed dialogs via the [OverlayArea] + * [ViewEnvironmentKey][com.squareup.workflow1.ui.ViewEnvironmentKey], + * which drives [ScreenOverlayDialogFactory.updateBounds]. + * + * @param cancelEvents function to be called when a modal session starts -- that is, + * when [update] is first called with a [ModalOverlay] member, or called again with + * one after calls with none. + * + * @param getParentLifecycleOwner provides the [LifecycleOwner] to serve as + * an ancestor to those created for managed [Dialog][android.app.Dialog]s. + * */ @WorkflowUiExperimentalApi -public class LayeredDialogs( +public class LayeredDialogs private constructor( private val context: Context, - private val modal: Boolean, + private val bounds: StateFlow, + private val cancelEvents: () -> Unit, private val getParentLifecycleOwner: () -> LifecycleOwner ) { - /** - * Builds a [LayeredDialogs] which looks through [view] to find its parent - * [LifecycleOwner][getParentLifecycleOwner]. - * - * @param modal When true, only the top-most dialog is allowed to process touch and key events - */ - public constructor( - view: View, - modal: Boolean - ) : this( - context = view.context, - modal = modal, - getParentLifecycleOwner = { - checkNotNull(ViewTreeLifecycleOwner.get(view)) { - "Expected a ViewTreeLifecycleOwner on $view" - } - } - ) - /** * Provides a new `ViewTreeSavedStateRegistryOwner` for each dialog, * which will save to the `ViewTreeSavedStateRegistryOwner` of this container view. @@ -55,8 +54,12 @@ public class LayeredDialogs( private var holders: List> = emptyList() - /** True when any dialogs are visible, or becoming visible. */ - public val hasDialogs: Boolean = holders.isNotEmpty() + public var allowEvents: Boolean = true + private set(value) { + val was = field + field = value + if (value != was) cancelEvents() + } /** * Updates the managed set of [Dialog][android.app.Dialog] instances to reflect @@ -67,21 +70,40 @@ public class LayeredDialogs( * is shown, and is destroyed when it is dismissed. Views nested in a managed dialog * can use [ViewTreeLifecycleOwner][androidx.lifecycle.ViewTreeLifecycleOwner] as * usual. + * + * @param updateBase function to be called before updating the dialogs. + * The [ViewEnvironment] passed to that function is enhanced with a [CoveredByModal] + * as appropriate, to ensure proper behavior of [allowEvents] of nested containers. */ public fun update( overlays: List, - viewEnvironment: ViewEnvironment + viewEnvironment: ViewEnvironment, + updateBase: (environment: ViewEnvironment) -> Unit ) { + val modalIndex = overlays.indexOfFirst { it is ModalOverlay } + val showingModals = modalIndex > -1 + + allowEvents = !showingModals + + updateBase( + if (showingModals) viewEnvironment + (CoveredByModal to true) else viewEnvironment + ) + + val envPlusBounds = viewEnvironment + OverlayArea(bounds) + // On each update we build a new list of the running dialogs, both the // existing ones and any new ones. We need this so that we can compare // it with the previous list, and see what dialogs close. val newHolders = mutableListOf>() for ((i, overlay) in overlays.withIndex()) { + val dialogEnv = + if (i < modalIndex) envPlusBounds + (CoveredByModal to true) else envPlusBounds + newHolders += if (i < holders.size && holders[i].canTakeRendering(overlay)) { // There is already a dialog at this index, and it is compatible // with the new Overlay at that index. Just update it. - holders[i].also { it.takeRendering(overlay, viewEnvironment) } + holders[i].also { it.takeRendering(overlay, dialogEnv) } } else { // We need a new dialog for this overlay. Time to build it. // We wrap our Dialog instances in DialogHolder to keep them @@ -90,11 +112,9 @@ public class LayeredDialogs( // decor view, more consistent with what ScreenViewFactory does, // but calling Window.getDecorView has side effects, and things // break if we call it to early. Need to store them somewhere else. - overlay.toDialogFactory(viewEnvironment).let { dialogFactory -> - DialogHolder( - overlay, viewEnvironment, i, modal, context, dialogFactory - ).also { newHolder -> - newHolder.takeRendering(overlay, viewEnvironment) + overlay.toDialogFactory(dialogEnv).let { dialogFactory -> + DialogHolder(overlay, dialogEnv, i, context, dialogFactory).also { newHolder -> + newHolder.takeRendering(overlay, dialogEnv) // Show the dialog, creating it if necessary. newHolder.show(getParentLifecycleOwner(), stateRegistryAggregator) @@ -115,20 +135,29 @@ public class LayeredDialogs( /** * Must be called whenever the owning view is [attached to a window][View.onAttachedToWindow]. - * Must eventually be matched with a call to [detachFromParentRegistry]. + * Must eventually be matched with a call to [onDetachedFromWindow]. + * + * @param savedStateParentKey Unique identifier for this view for SavedStateRegistry purposes. + * Typically based on the [Compatible.keyFor] the current rendering. Taking this approach + * allows feature developers to take control over naming, e.g. by wrapping renderings + * with [NamedScreen][com.squareup.workflow1.ui.NamedScreen]. */ - public fun attachToParentRegistryOwner( - key: String, - parentOwner: SavedStateRegistryOwner + public fun onAttachedToWindow( + savedStateParentKey: String, + view: View ) { - stateRegistryAggregator.attachToParentRegistry(key, parentOwner) + stateRegistryAggregator.attachToParentRegistry( + savedStateParentKey, + WorkflowAndroidXSupport.stateRegistryOwnerFromViewTreeOrContext(view) + ) } /** - * Must be called whenever the owning view is [detached from a window][View.onDetachedFromWindow]. - * Must be matched with a call to [attachToParentRegistryOwner]. + * Must be called whenever the owning view is + * [detached from a window][View.onDetachedFromWindow]. + * Must be matched with a call to [onAttachedToWindow]. */ - public fun detachFromParentRegistry() { + public fun onDetachedFromWindow() { stateRegistryAggregator.detachFromParentRegistry() } @@ -173,4 +202,80 @@ public class LayeredDialogs( override fun newArray(size: Int): Array = arrayOfNulls(size) } } + + public companion object { + /** + * Creates a [LayeredDialogs] instance based on the given [view], which will + * serve as the source for a [LifecycleOwner], and whose bounds will be reported + * via [OverlayArea]. + * + * - The [view]'s [dispatchTouchEvent][View.dispatchTouchEvent] and + * [dispatchKeyEvent][View.dispatchKeyEvent] methods should be overridden + * to honor [LayeredDialogs.allowEvents]. + * + * - The [view]'s [onAttachedToWindow][View.onAttachedToWindow] and + * [onDetachedFromWindow][View.onDetachedFromWindow] methods must call + * through to the like named methods of the returned [LayeredDialogs] + * ([onAttachedToWindow], [onDetachedFromWindow]). + * + * - The [view]'s [onSaveInstanceState][View.onSaveInstanceState] and + * [onRestoreInstanceState][View.onRestoreInstanceState] methods must call + * through to the like named methods of the returned [LayeredDialogs] + * ([onSaveInstanceState], [onRestoreInstanceState]). + */ + public fun forView( + view: View, + superDispatchTouchEvent: (MotionEvent) -> Unit + ): LayeredDialogs { + val boundsRect = Rect() + if (view.isAttachedToWindow) view.getGlobalVisibleRect(boundsRect) + val bounds = MutableStateFlow(Rect(boundsRect)) + + return LayeredDialogs( + context = view.context, + bounds = bounds, + cancelEvents = { + // Note similar code in DialogHolder. + + // https://stackoverflow.com/questions/2886407/dealing-with-rapid-tapping-on-buttons + // If any motion events were enqueued on the main thread, cancel them. + dispatchCancelEvent { superDispatchTouchEvent(it) } + // When we cancel, have to warn things like RecyclerView that handle streams + // of motion events and eventually dispatch input events (click, key pressed, etc.) + // based on them. + view.cancelPendingInputEvents() + } + ) { + checkNotNull(ViewTreeLifecycleOwner.get(view)) { + "Expected a ViewTreeLifecycleOwner on $view" + } + }.also { dialogs -> + + val boundsListener = OnGlobalLayoutListener { + if (view.getGlobalVisibleRect(boundsRect) && boundsRect != bounds.value) { + bounds.value = Rect(boundsRect) + } + // Should we close the dialogs if getGlobalVisibleRect returns false? + // https://github.com/square/workflow-kotlin/issues/599 + } + + val attachStateChangeListener = object : OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + boundsListener.onGlobalLayout() + v.viewTreeObserver.addOnGlobalLayoutListener(boundsListener) + } + + override fun onViewDetachedFromWindow(v: View) { + // Don't leak the dialogs if we're suddenly yanked out of view. + // https://github.com/square/workflow-kotlin/issues/314 + dialogs.update(emptyList(), ViewEnvironment.EMPTY) {} + v.viewTreeObserver.removeOnGlobalLayoutListener(boundsListener) + bounds.value = Rect() + } + } + + view.addOnAttachStateChangeListener(attachStateChangeListener) + } + } + } } diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalArea.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalArea.kt deleted file mode 100644 index fefa29336f..0000000000 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalArea.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.squareup.workflow1.ui.container - -import android.graphics.Rect -import com.squareup.workflow1.ui.ViewEnvironment -import com.squareup.workflow1.ui.ViewEnvironmentKey -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow - -/** - * Reports the the area of the screen whose events should be blocked by any modal [Overlay]. - * Expected to be supplied by containers that support [BodyAndModalsScreen]. - */ -@WorkflowUiExperimentalApi -internal class ModalArea( - val bounds: StateFlow -) { - companion object : ViewEnvironmentKey(type = ModalArea::class) { - override val default: ModalArea = ModalArea(MutableStateFlow(Rect())) - } -} - -@WorkflowUiExperimentalApi -internal operator fun ViewEnvironment.plus(modalArea: ModalArea): ViewEnvironment = - this + (ModalArea to modalArea) diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed.kt index 711f5a0e2f..eab3dae24c 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayOnBackPressed.kt @@ -8,7 +8,7 @@ import com.squareup.workflow1.ui.onBackPressedDispatcherOwnerOrNull /** * The function called to handle back button events in [Dialog][android.app.Dialog]s built - * by [ModalScreenOverlayDialogFactory]. The default implementation uses the + * by [ScreenOverlayDialogFactory]. The default implementation uses the * [Activity][android.app.Activity]'s * [OnBackPressedDispatcher][androidx.activity.OnBackPressedDispatcher]. * @@ -19,7 +19,7 @@ import com.squareup.workflow1.ui.onBackPressedDispatcherOwnerOrNull public fun interface ModalScreenOverlayOnBackPressed { /** * Called when the device back button is pressed and a dialog built by a - * [ModalScreenOverlayDialogFactory] has window focus. + * [ScreenOverlayDialogFactory] has window focus. * * @return true if the back press event was consumed */ diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayArea.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayArea.kt new file mode 100644 index 0000000000..249fbed9a9 --- /dev/null +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayArea.kt @@ -0,0 +1,26 @@ +package com.squareup.workflow1.ui.container + +import android.graphics.Rect +import com.squareup.workflow1.ui.ViewEnvironment +import com.squareup.workflow1.ui.ViewEnvironmentKey +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** + * Reports the the area of the screen whose events should be blocked by any modal [Overlay], + * in the style reported by [View.getGlobalVisibleRect][android.view.View.getGlobalVisibleRect]. + * Expected to be supplied by containers that support [BodyAndOverlaysScreen]. + */ +@WorkflowUiExperimentalApi +internal class OverlayArea( + val bounds: StateFlow +) { + companion object : ViewEnvironmentKey(type = OverlayArea::class) { + override val default: OverlayArea = OverlayArea(MutableStateFlow(Rect())) + } +} + +@WorkflowUiExperimentalApi +internal operator fun ViewEnvironment.plus(overlayArea: OverlayArea): ViewEnvironment = + this + (OverlayArea to overlayArea) diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayDialogFactoryFinder.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayDialogFactoryFinder.kt index 7167c0074b..e204e8fca0 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayDialogFactoryFinder.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/OverlayDialogFactoryFinder.kt @@ -8,9 +8,9 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi /** * [ViewEnvironment] service object used by [Overlay.toDialogFactory] to find the right * [OverlayDialogFactoryScreenViewFactory]. The default implementation makes [AndroidOverlay] - * work, and provides default bindings for [AlertOverlay] and [ModalScreenOverlay]. + * work, and provides default bindings for [AlertOverlay] and [FullScreenOverlay]. * - * Here is how this hook could be used to provide a custom dialog to handle [ModalScreenOverlay]: + * Here is how this hook could be used to provide a custom dialog to handle [FullScreenOverlay]: * * class MyDialogFactory : ModalScreenOverlayDialogFactory>( * ModalScreenOverlay::class @@ -58,9 +58,9 @@ public interface OverlayDialogFactoryFinder { ?: (rendering as? AlertOverlay)?.let { AlertOverlayDialogFactory() as OverlayDialogFactory } - ?: (rendering as? ModalScreenOverlay<*>)?.let { - ModalScreenOverlayDialogFactory>( - ModalScreenOverlay::class + ?: (rendering as? FullScreenOverlay<*>)?.let { + ScreenOverlayDialogFactory>( + FullScreenOverlay::class ) as OverlayDialogFactory } ?: throw IllegalArgumentException( diff --git a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayDialogFactory.kt b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ScreenOverlayDialogFactory.kt similarity index 82% rename from workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayDialogFactory.kt rename to workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ScreenOverlayDialogFactory.kt index 875aeff39e..cf79d28c22 100644 --- a/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlayDialogFactory.kt +++ b/workflow-ui/core-android/src/main/java/com/squareup/workflow1/ui/container/ScreenOverlayDialogFactory.kt @@ -24,22 +24,21 @@ import com.squareup.workflow1.ui.toViewFactory import kotlin.reflect.KClass /** - * Default [OverlayDialogFactory] for [ModalScreenOverlay]. - * - * This class is non-final for ease of customization of [ModalScreenOverlay] handling, - * see [OverlayDialogFactoryFinder] for details. It is also convenient to use as a - * base class for custom [ScreenOverlay] rendering types. + * Extensible base implementation of [OverlayDialogFactory] for [ScreenOverlay] + * types. Also serves as the default factory for [FullScreenOverlay]. + * (See [OverlayDialogFactoryFinder] for guidance on customizing the presentation + * of [FullScreenOverlay].) * * Dialogs built by this class are compatible with [View.backPressedHandler], and - * honor the [ModalArea] constraint placed in the [ViewEnvironment] by the - * standard [BodyAndModalsScreen] container. + * honor the [OverlayArea] constraint placed in the [ViewEnvironment] by the + * standard [BodyAndOverlaysScreen] container. * * Ironically, [Dialog] instances are created with [FLAG_NOT_TOUCH_MODAL], to ensure * that events outside of the bounds reported by [updateBounds] reach views in * lower windows. See that method for details. */ @WorkflowUiExperimentalApi -public open class ModalScreenOverlayDialogFactory>( +public open class ScreenOverlayDialogFactory>( override val type: KClass ) : OverlayDialogFactory { @@ -56,17 +55,16 @@ public open class ModalScreenOverlayDialogFactory>( } /** - * If the [ScreenOverlay] displayed by a [dialog] created by this - * factory is contained in a [BodyAndModalsScreen], this method will - * be called to report the bounds of the managing view, as reported by [ModalArea]. - * It is expected that such a dialog will be restricted to those bounds. + * This method will be called to report the bounds of the managing container view, + * as reported by [OverlayArea]. Well behaved [ScreenOverlay] dialogs are expected to + * be restricted to those bounds. * * Honoring this contract makes it easy to define areas of the display * that are outside of the "shadow" of a modal dialog. Imagine an app * with a status bar that should not be covered by modals. * - * The default implementation calls straight through to [Dialog.setBounds]. - * Custom implementations are not required to call `super`. + * The default implementation calls straight through to the provided [Dialog.setBounds] + * function. Custom implementations are not required to call `super`. * * @see Dialog.setBounds */ @@ -109,8 +107,8 @@ public open class ModalScreenOverlayDialogFactory>( event.action == ACTION_UP return when { - isBackPress -> contentViewHolder.environment.get(ModalScreenOverlayOnBackPressed) - .onBackPressed(contentViewHolder.view) == true + isBackPress -> contentViewHolder.environment[ModalScreenOverlayOnBackPressed] + .onBackPressed(contentViewHolder.view) else -> realWindowCallback.dispatchKeyEvent(event) } } @@ -141,7 +139,7 @@ public open class ModalScreenOverlayDialogFactory>( } /** - * The default implementation of [ModalScreenOverlayDialogFactory.buildDialogWithContentView]. + * The default implementation of [ScreenOverlayDialogFactory.buildDialogWithContentView]. * * Sets the [background][Window.setBackgroundDrawable] of the receiver's [Window] based * on its theme, if any, or else `null`. (Setting the background to `null` ensures the window diff --git a/workflow-ui/core-common/api/core-common.api b/workflow-ui/core-common/api/core-common.api index fc5a31fe09..91ee62e8fb 100644 --- a/workflow-ui/core-common/api/core-common.api +++ b/workflow-ui/core-common/api/core-common.api @@ -139,7 +139,7 @@ public final class com/squareup/workflow1/ui/ViewRegistryKt { public abstract interface annotation class com/squareup/workflow1/ui/WorkflowUiExperimentalApi : java/lang/annotation/Annotation { } -public final class com/squareup/workflow1/ui/container/AlertOverlay : com/squareup/workflow1/ui/container/Overlay { +public final class com/squareup/workflow1/ui/container/AlertOverlay : com/squareup/workflow1/ui/container/ModalOverlay { public fun (Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)V public synthetic fun (Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/util/Map; @@ -205,14 +205,14 @@ public final class com/squareup/workflow1/ui/container/BackStackScreenKt { public static final fun toBackStackScreenOrNull (Ljava/util/List;)Lcom/squareup/workflow1/ui/container/BackStackScreen; } -public final class com/squareup/workflow1/ui/container/BodyAndModalsScreen : com/squareup/workflow1/ui/Screen { +public final class com/squareup/workflow1/ui/container/BodyAndOverlaysScreen : com/squareup/workflow1/ui/Screen { public fun (Lcom/squareup/workflow1/ui/Screen;Ljava/util/List;)V public synthetic fun (Lcom/squareup/workflow1/ui/Screen;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun (Lcom/squareup/workflow1/ui/Screen;[Lcom/squareup/workflow1/ui/container/Overlay;)V public final fun getBody ()Lcom/squareup/workflow1/ui/Screen; - public final fun getModals ()Ljava/util/List; - public final fun mapBody (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/container/BodyAndModalsScreen; - public final fun mapModals (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/container/BodyAndModalsScreen; + public final fun getOverlays ()Ljava/util/List; + public final fun mapBody (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/container/BodyAndOverlaysScreen; + public final fun mapModals (Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/ui/container/BodyAndOverlaysScreen; } public final class com/squareup/workflow1/ui/container/EnvironmentScreen : com/squareup/workflow1/ui/Compatible, com/squareup/workflow1/ui/Screen { @@ -229,12 +229,15 @@ public final class com/squareup/workflow1/ui/container/EnvironmentScreenKt { public static final fun withRegistry (Lcom/squareup/workflow1/ui/Screen;Lcom/squareup/workflow1/ui/ViewRegistry;)Lcom/squareup/workflow1/ui/container/EnvironmentScreen; } -public final class com/squareup/workflow1/ui/container/ModalScreenOverlay : com/squareup/workflow1/ui/container/ScreenOverlay { +public final class com/squareup/workflow1/ui/container/FullScreenOverlay : com/squareup/workflow1/ui/container/ScreenOverlay { public fun (Lcom/squareup/workflow1/ui/Screen;)V public fun getCompatibilityKey ()Ljava/lang/String; public fun getContent ()Lcom/squareup/workflow1/ui/Screen; } +public abstract interface class com/squareup/workflow1/ui/container/ModalOverlay : com/squareup/workflow1/ui/container/Overlay { +} + public abstract interface class com/squareup/workflow1/ui/container/Overlay { } diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/AlertOverlay.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/AlertOverlay.kt index c6576fb43e..d2018ed3d2 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/AlertOverlay.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/AlertOverlay.kt @@ -14,7 +14,7 @@ public data class AlertOverlay( val title: String = "", val cancelable: Boolean = true, val onEvent: (Event) -> Unit -) : Overlay { +) : ModalOverlay { public enum class Button { POSITIVE, NEGATIVE, diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/BodyAndModalsScreen.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/BodyAndModalsScreen.kt deleted file mode 100644 index 1839abb452..0000000000 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/BodyAndModalsScreen.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.squareup.workflow1.ui.container - -import com.squareup.workflow1.ui.Screen -import com.squareup.workflow1.ui.WorkflowUiExperimentalApi - -/** - * A screen that may stack a number of modal [Overlay]s over a body. - * While modals are present, the body is expected to ignore any - * input events -- touch, keyboard, etc. - * - * UI kits are expected to provide handling for this class by default. - */ -@WorkflowUiExperimentalApi -public class BodyAndModalsScreen( - public val body: B, - public val modals: List = emptyList() -) : Screen { - public constructor( - body: B, - vararg modals: M - ) : this(body, modals.toList()) - - public fun mapBody(transform: (B) -> S): BodyAndModalsScreen { - return BodyAndModalsScreen(transform(body), modals) - } - - public fun mapModals(transform: (M) -> O): BodyAndModalsScreen { - return BodyAndModalsScreen(body, modals.map(transform)) - } -} diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/BodyAndOverlaysScreen.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/BodyAndOverlaysScreen.kt new file mode 100644 index 0000000000..3e0a92a1a3 --- /dev/null +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/BodyAndOverlaysScreen.kt @@ -0,0 +1,31 @@ +package com.squareup.workflow1.ui.container + +import com.squareup.workflow1.ui.Screen +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +/** + * A screen that may stack a number of [Overlay]s over a body. + * If any members of [overlays] are [ModalOverlay], the body and + * lower-indexed members of that list are expected to ignore input + * events -- touch, keyboard, etc. + * + * UI kits are expected to provide handling for this class by default. + */ +@WorkflowUiExperimentalApi +public class BodyAndOverlaysScreen( + public val body: B, + public val overlays: List = emptyList() +) : Screen { + public constructor( + body: B, + vararg modals: O + ) : this(body, modals.toList()) + + public fun mapBody(transform: (B) -> S): BodyAndOverlaysScreen { + return BodyAndOverlaysScreen(transform(body), overlays) + } + + public fun mapModals(transform: (O) -> N): BodyAndOverlaysScreen { + return BodyAndOverlaysScreen(body, overlays.map(transform)) + } +} diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlay.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/FullScreenOverlay.kt similarity index 69% rename from workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlay.kt rename to workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/FullScreenOverlay.kt index 2591ecca61..e0130762af 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/ModalScreenOverlay.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/FullScreenOverlay.kt @@ -4,11 +4,11 @@ import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.WorkflowUiExperimentalApi /** - * A basic [ScreenOverlay] that fills all available space. + * A basic [ScreenOverlay] that covers its container with the wrapped [content] [Screen]. * * UI kits are expected to provide handling for this class by default. */ @WorkflowUiExperimentalApi -public class ModalScreenOverlay( +public class FullScreenOverlay( public override val content: ContentT ) : ScreenOverlay diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/ModalOverlay.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/ModalOverlay.kt new file mode 100644 index 0000000000..9b0cceeb7e --- /dev/null +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/ModalOverlay.kt @@ -0,0 +1,10 @@ +package com.squareup.workflow1.ui.container + +import com.squareup.workflow1.ui.WorkflowUiExperimentalApi + +/** + * Marker interface identifying [Overlay] renderings whose presence + * indicates that events are blocked from lower layers. + */ +@WorkflowUiExperimentalApi +public interface ModalOverlay : Overlay diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/Overlay.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/Overlay.kt index a8311ec637..282635903c 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/Overlay.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/container/Overlay.kt @@ -6,13 +6,11 @@ import com.squareup.workflow1.ui.WorkflowUiExperimentalApi * Marker interface implemented by window-like renderings that map to a layer above * a base [Screen][com.squareup.workflow1.ui.Screen]. * - * Note that an Overlay is not necessarily a modal window, though that is how - * they are used in [BodyAndModalsScreen]. An Overlay can be any part of the UI - * that visually floats in a layer above the main UI, or above other Overlays. - * Possible examples include alerts, drawers, and tooltips. + * An Overlay can be any part of the UI that visually floats in a layer above the main UI, + * or above other Overlays. Possible examples include alerts, drawers, and tooltips. * - * Whether or not an Overlay's presence indicates that events are blocked from lower - * layers is a separate concern. + * Note in particular that an Overlay is not necessarily a modal window. + * Rendering types can opt into modality by extending [ModalOverlay]. */ @WorkflowUiExperimentalApi public interface Overlay