Skip to content

Commit

Permalink
Guest-side API for new insets
Browse files Browse the repository at this point in the history
  • Loading branch information
squarejesse committed Nov 20, 2024
1 parent d08cbe3 commit eced783
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 0 deletions.
2 changes: 2 additions & 0 deletions redwood-compose/api/android/redwood-compose.api
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ public final class app/cash/redwood/compose/RedwoodCompositionKt {
}

public final class app/cash/redwood/compose/UiConfigurationKt {
public static final fun ConsumeInsets (Lapp/cash/redwood/ui/Margin;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
public static final fun getCurrent (Lapp/cash/redwood/ui/UiConfiguration$Companion;Landroidx/compose/runtime/Composer;I)Lapp/cash/redwood/ui/UiConfiguration;
public static final fun getLocalUiConfiguration ()Landroidx/compose/runtime/ProvidableCompositionLocal;
public static final fun getLocalViewInsets ()Landroidx/compose/runtime/ProvidableCompositionLocal;
}

public final class app/cash/redwood/compose/WidgetVersionKt {
Expand Down
2 changes: 2 additions & 0 deletions redwood-compose/api/jvm/redwood-compose.api
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ public final class app/cash/redwood/compose/RedwoodCompositionKt {
}

public final class app/cash/redwood/compose/UiConfigurationKt {
public static final fun ConsumeInsets (Lapp/cash/redwood/ui/Margin;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
public static final fun getCurrent (Lapp/cash/redwood/ui/UiConfiguration$Companion;Landroidx/compose/runtime/Composer;I)Lapp/cash/redwood/ui/UiConfiguration;
public static final fun getLocalUiConfiguration ()Landroidx/compose/runtime/ProvidableCompositionLocal;
public static final fun getLocalViewInsets ()Landroidx/compose/runtime/ProvidableCompositionLocal;
}

public final class app/cash/redwood/compose/WidgetVersionKt {
Expand Down
3 changes: 3 additions & 0 deletions redwood-compose/api/redwood-compose.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ final val app.cash.redwood.compose/LocalOnBackPressedDispatcher // app.cash.redw
final fun <get-LocalOnBackPressedDispatcher>(): androidx.compose.runtime/ProvidableCompositionLocal<app.cash.redwood.ui/OnBackPressedDispatcher> // app.cash.redwood.compose/LocalOnBackPressedDispatcher.<get-LocalOnBackPressedDispatcher>|<get-LocalOnBackPressedDispatcher>(){}[0]
final val app.cash.redwood.compose/LocalUiConfiguration // app.cash.redwood.compose/LocalUiConfiguration|{}LocalUiConfiguration[0]
final fun <get-LocalUiConfiguration>(): androidx.compose.runtime/ProvidableCompositionLocal<app.cash.redwood.ui/UiConfiguration> // app.cash.redwood.compose/LocalUiConfiguration.<get-LocalUiConfiguration>|<get-LocalUiConfiguration>(){}[0]
final val app.cash.redwood.compose/LocalViewInsets // app.cash.redwood.compose/LocalViewInsets|{}LocalViewInsets[0]
final fun <get-LocalViewInsets>(): androidx.compose.runtime/ProvidableCompositionLocal<app.cash.redwood.ui/Margin> // app.cash.redwood.compose/LocalViewInsets.<get-LocalViewInsets>|<get-LocalViewInsets>(){}[0]
final val app.cash.redwood.compose/LocalWidgetVersion // app.cash.redwood.compose/LocalWidgetVersion|{}LocalWidgetVersion[0]
final fun <get-LocalWidgetVersion>(): androidx.compose.runtime/ProvidableCompositionLocal<kotlin/UInt> // app.cash.redwood.compose/LocalWidgetVersion.<get-LocalWidgetVersion>|<get-LocalWidgetVersion>(){}[0]
final val app.cash.redwood.compose/WidgetVersion // app.cash.redwood.compose/WidgetVersion|{}WidgetVersion[0]
Expand All @@ -32,6 +34,7 @@ final val app.cash.redwood.compose/current // app.cash.redwood.compose/current|@
final fun <#A: kotlin/Any> app.cash.redwood.compose/RedwoodComposition(kotlinx.coroutines/CoroutineScope, app.cash.redwood.widget/RedwoodView<#A>, app.cash.redwood.widget/WidgetSystem<#A>, kotlin/Function0<kotlin/Unit> = ...): app.cash.redwood.compose/RedwoodComposition // app.cash.redwood.compose/RedwoodComposition|RedwoodComposition(kotlinx.coroutines.CoroutineScope;app.cash.redwood.widget.RedwoodView<0:0>;app.cash.redwood.widget.WidgetSystem<0:0>;kotlin.Function0<kotlin.Unit>){0§<kotlin.Any>}[0]
final fun <#A: kotlin/Any> app.cash.redwood.compose/RedwoodComposition(kotlinx.coroutines/CoroutineScope, app.cash.redwood.widget/Widget.Children<#A>, app.cash.redwood.ui/OnBackPressedDispatcher, androidx.compose.runtime.saveable/SaveableStateRegistry?, kotlinx.coroutines.flow/StateFlow<app.cash.redwood.ui/UiConfiguration>, app.cash.redwood.widget/WidgetSystem<#A>, kotlin/Function0<kotlin/Unit> = ...): app.cash.redwood.compose/RedwoodComposition // app.cash.redwood.compose/RedwoodComposition|RedwoodComposition(kotlinx.coroutines.CoroutineScope;app.cash.redwood.widget.Widget.Children<0:0>;app.cash.redwood.ui.OnBackPressedDispatcher;androidx.compose.runtime.saveable.SaveableStateRegistry?;kotlinx.coroutines.flow.StateFlow<app.cash.redwood.ui.UiConfiguration>;app.cash.redwood.widget.WidgetSystem<0:0>;kotlin.Function0<kotlin.Unit>){0§<kotlin.Any>}[0]
final fun app.cash.redwood.compose/BackHandler(kotlin/Boolean, kotlin/Function0<kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // app.cash.redwood.compose/BackHandler|BackHandler(kotlin.Boolean;kotlin.Function0<kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun app.cash.redwood.compose/ConsumeInsets(app.cash.redwood.ui/Margin?, kotlin/Function3<app.cash.redwood.ui/Margin, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // app.cash.redwood.compose/ConsumeInsets|ConsumeInsets(app.cash.redwood.ui.Margin?;kotlin.Function3<app.cash.redwood.ui.Margin,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun app.cash.redwood.compose/app_cash_redwood_compose_ChildrenNode$stableprop_getter(): kotlin/Int // app.cash.redwood.compose/app_cash_redwood_compose_ChildrenNode$stableprop_getter|app_cash_redwood_compose_ChildrenNode$stableprop_getter(){}[0]
final fun app.cash.redwood.compose/app_cash_redwood_compose_NodeApplier$stableprop_getter(): kotlin/Int // app.cash.redwood.compose/app_cash_redwood_compose_NodeApplier$stableprop_getter|app_cash_redwood_compose_NodeApplier$stableprop_getter(){}[0]
final fun app.cash.redwood.compose/app_cash_redwood_compose_RedwoodComposeContent$stableprop_getter(): kotlin/Int // app.cash.redwood.compose/app_cash_redwood_compose_RedwoodComposeContent$stableprop_getter|app_cash_redwood_compose_RedwoodComposeContent$stableprop_getter(){}[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@
package app.cash.redwood.compose

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.compositionLocalWithComputedDefaultOf
import app.cash.redwood.ui.Margin
import app.cash.redwood.ui.UiConfiguration
import app.cash.redwood.ui.dp

/**
* Provide various configurations of the UI.
Expand All @@ -43,3 +47,64 @@ public val UiConfiguration.Companion.current: UiConfiguration
@Composable
@ReadOnlyComposable
get() = LocalUiConfiguration.current

/**
* The insets of the viewport that the composition is not safe for content because it is obscured
* by the system.
*
* To avoid double-consuming insets, consume them using [ConsumeInsets].
*/
public val LocalViewInsets: ProvidableCompositionLocal<Margin> =
compositionLocalWithComputedDefaultOf {
LocalUiConfiguration.currentValue.viewInsets
}

/**
* Consume [LocalViewInsets] for the execution of [block]. This will consume all available insets
* unless [maximumValue] is set.
*
* The parameter to [block] is the actual insets that were consumed.
*
* Note that this function **does not** apply to the code preceding or following the caller, so it
* is possible for insets to be consumed multiple times. In layouts like `Box`, this may be the
* desired behavior.
*/
@Composable
public fun ConsumeInsets(
maximumValue: Margin? = null,
block: @Composable (Margin) -> Unit,
) {
val previous = LocalViewInsets.current

val consumed: Margin
val updated: Margin
if (maximumValue != null) {
consumed = previous.coerceAtMost(maximumValue)
updated = previous - consumed
} else {
consumed = previous
updated = Margin.Zero
}

CompositionLocalProvider(LocalViewInsets provides updated) {
block(consumed)
}
}

private fun Margin.coerceAtMost(maximumValue: Margin): Margin {
return Margin(
start = start.value.coerceAtMost(maximumValue.start.value).dp,
end = end.value.coerceAtMost(maximumValue.end.value).dp,
top = top.value.coerceAtMost(maximumValue.top.value).dp,
bottom = bottom.value.coerceAtMost(maximumValue.bottom.value).dp,
)
}

private operator fun Margin.minus(other: Margin): Margin {
return Margin(
start = (start.value - other.start.value).dp,
end = (end.value - other.end.value).dp,
top = (top.value - other.top.value).dp,
bottom = (bottom.value - other.bottom.value).dp,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* Copyright (C) 2024 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package app.cash.redwood.compose

import app.cash.redwood.ui.Margin
import app.cash.redwood.ui.dp
import assertk.assertThat
import assertk.assertions.containsExactly
import com.example.redwood.testapp.compose.Text
import com.example.redwood.testapp.testing.TestSchemaTester
import com.example.redwood.testapp.testing.TextValue
import kotlin.test.Test
import kotlinx.coroutines.test.runTest

class LocalViewInsetsTest {
@Test
fun localInsetsUpdated() = runTest {
TestSchemaTester {
uiConfigurations.value = uiConfigurations.value.copy(
viewInsets = Margin(top = 30.dp),
)

setContent {
Text("available=${LocalViewInsets.current}")
}

assertThat(awaitSnapshot()).containsExactly(
TextValue(text = "available=${Margin(top = 30.0.dp)}"),
)

uiConfigurations.value = uiConfigurations.value.copy(
viewInsets = Margin(top = 20.dp),
)

assertThat(awaitSnapshot()).containsExactly(
TextValue(text = "available=${Margin(top = 20.0.dp)}"),
)
}
}

@Test
fun consumeInsets() = runTest {
TestSchemaTester {
uiConfigurations.value = uiConfigurations.value.copy(
viewInsets = Margin(top = 30.dp),
)

setContent {
Text("before available=${LocalViewInsets.current}")

ConsumeInsets { consumed ->
Text("child consumed=$consumed")
Text("child available=${LocalViewInsets.current}")
}

Text("after available=${LocalViewInsets.current}")
}

assertThat(awaitSnapshot()).containsExactly(
TextValue(text = "before available=${Margin(top = 30.0.dp)}"),
TextValue(text = "child consumed=${Margin(top = 30.0.dp)}"),
TextValue(text = "child available=${Margin(top = 0.0.dp)}"),
TextValue(text = "after available=${Margin(top = 30.0.dp)}"),
)
}
}

@Test
fun consumeInsetsWithMax() = runTest {
TestSchemaTester {
uiConfigurations.value = uiConfigurations.value.copy(
viewInsets = Margin(top = 30.dp),
)

setContent {
Text("before available=${LocalViewInsets.current}")

ConsumeInsets(maximumValue = Margin(top = 10.0.dp)) { consumed ->
Text("child consumed=$consumed")
Text("child available=${LocalViewInsets.current}")
}

Text("after available=${LocalViewInsets.current}")
}

assertThat(awaitSnapshot()).containsExactly(
TextValue(text = "before available=${Margin(top = 30.0.dp)}"),
TextValue(text = "child consumed=${Margin(top = 10.0.dp)}"),
TextValue(text = "child available=${Margin(top = 20.0.dp)}"),
TextValue(text = "after available=${Margin(top = 30.0.dp)}"),
)
}
}

@Test
fun consumeAllInsetsWithMaxAndUpdates() = runTest {
TestSchemaTester {
uiConfigurations.value = uiConfigurations.value.copy(
viewInsets = Margin(10.0.dp, 20.0.dp, 30.0.dp, 40.0.dp),
)

setContent {
ConsumeInsets(maximumValue = Margin(all = 15.dp)) { consumed ->
Text("consumed=$consumed")
Text("available=${LocalViewInsets.current}")
}
}

assertThat(awaitSnapshot()).containsExactly(
TextValue(text = "consumed=${Margin(10.0.dp, 15.0.dp, 15.0.dp, 15.0.dp)}"),
TextValue(text = "available=${Margin(0.0.dp, 5.0.dp, 15.0.dp, 25.0.dp)}"),
)

uiConfigurations.value = uiConfigurations.value.copy(
viewInsets = Margin(11.0.dp, 21.0.dp, 31.0.dp, 41.0.dp),
)

assertThat(awaitSnapshot()).containsExactly(
TextValue(text = "consumed=${Margin(11.0.dp, 15.0.dp, 15.0.dp, 15.0.dp)}"),
TextValue(text = "available=${Margin(0.0.dp, 6.0.dp, 16.0.dp, 26.0.dp)}"),
)
}
}
}

0 comments on commit eced783

Please sign in to comment.