Skip to content

Commit

Permalink
Add UiConfiguration.layoutDirection and populate it on all platforms (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
drewhamilton authored Jun 10, 2024
1 parent 51b797c commit 41b14f6
Show file tree
Hide file tree
Showing 13 changed files with 153 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ New:
- Added a basic DOM-based LazyList implementation.
-`TreehouseApp.close()` stops the app and prevents it from being started again later.
-`Widget.detach()` is called when a widget will receive no further updates from the presenter.
- Added `UiConfiguration.layoutDirection` to support reading the host's layout direction.

Changed:
- Removed deprecated `typealias`es for generated `-WidgetFactories` type which was renamed to `-WidgetSystem` in 0.10.0.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import app.cash.redwood.compose.RedwoodComposition
import app.cash.redwood.ui.Density
import app.cash.redwood.ui.LayoutDirection as RedwoodLayoutDirection
import app.cash.redwood.ui.OnBackPressedDispatcher
import app.cash.redwood.ui.Size
import app.cash.redwood.ui.UiConfiguration
Expand All @@ -55,6 +58,10 @@ public fun RedwoodContent(
safeAreaInsets = safeAreaInsets(),
viewportSize = viewportSize,
density = density.density.toDouble(),
layoutDirection = when (LocalLayoutDirection.current) {
LayoutDirection.Ltr -> RedwoodLayoutDirection.Ltr
LayoutDirection.Rtl -> RedwoodLayoutDirection.Rtl
},
)
val uiConfigurations = remember { MutableStateFlow(uiConfiguration) }
LaunchedEffect(uiConfiguration) {
Expand Down
14 changes: 12 additions & 2 deletions redwood-runtime/api/android/redwood-runtime.api
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,15 @@ public final class app/cash/redwood/ui/DpKt {
public static final fun toPlatformDp-mnpKzHI (D)D
}

public final class app/cash/redwood/ui/LayoutDirection : java/lang/Enum {
public static final field Auto Lapp/cash/redwood/ui/LayoutDirection;
public static final field Ltr Lapp/cash/redwood/ui/LayoutDirection;
public static final field Rtl Lapp/cash/redwood/ui/LayoutDirection;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lapp/cash/redwood/ui/LayoutDirection;
public static fun values ()[Lapp/cash/redwood/ui/LayoutDirection;
}

public final class app/cash/redwood/ui/Margin {
public static final field $stable I
public static final field Companion Lapp/cash/redwood/ui/Margin$Companion;
Expand Down Expand Up @@ -205,11 +214,12 @@ 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 <init> ()V
public fun <init> (ZLapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Size;D)V
public synthetic fun <init> (ZLapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Size;DILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (ZLapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Size;DLapp/cash/redwood/ui/LayoutDirection;)V
public synthetic fun <init> (ZLapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Size;DLapp/cash/redwood/ui/LayoutDirection;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
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 fun hashCode ()I
Expand Down
14 changes: 12 additions & 2 deletions redwood-runtime/api/jvm/redwood-runtime.api
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,15 @@ public final class app/cash/redwood/ui/DpKt {
public static final fun toPlatformDp-mnpKzHI (D)D
}

public final class app/cash/redwood/ui/LayoutDirection : java/lang/Enum {
public static final field Auto Lapp/cash/redwood/ui/LayoutDirection;
public static final field Ltr Lapp/cash/redwood/ui/LayoutDirection;
public static final field Rtl Lapp/cash/redwood/ui/LayoutDirection;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lapp/cash/redwood/ui/LayoutDirection;
public static fun values ()[Lapp/cash/redwood/ui/LayoutDirection;
}

public final class app/cash/redwood/ui/Margin {
public static final field $stable I
public static final field Companion Lapp/cash/redwood/ui/Margin$Companion;
Expand Down Expand Up @@ -201,11 +210,12 @@ 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 <init> ()V
public fun <init> (ZLapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Size;D)V
public synthetic fun <init> (ZLapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Size;DILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (ZLapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Size;DLapp/cash/redwood/ui/LayoutDirection;)V
public synthetic fun <init> (ZLapp/cash/redwood/ui/Margin;Lapp/cash/redwood/ui/Size;DLapp/cash/redwood/ui/LayoutDirection;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
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 fun hashCode ()I
Expand Down
14 changes: 13 additions & 1 deletion redwood-runtime/api/redwood-runtime.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ final class app.cash.redwood.ui/Size { // app.cash.redwood.ui/Size|null[0]
final fun <get-width>(): app.cash.redwood.ui/Dp // app.cash.redwood.ui/Size.width.<get-width>|<get-width>(){}[0]
}
final class app.cash.redwood.ui/UiConfiguration { // app.cash.redwood.ui/UiConfiguration|null[0]
constructor <init>(kotlin/Boolean = ..., app.cash.redwood.ui/Margin = ..., app.cash.redwood.ui/Size = ..., kotlin/Double = ...) // app.cash.redwood.ui/UiConfiguration.<init>|<init>(kotlin.Boolean;app.cash.redwood.ui.Margin;app.cash.redwood.ui.Size;kotlin.Double){}[0]
constructor <init>(kotlin/Boolean = ..., app.cash.redwood.ui/Margin = ..., app.cash.redwood.ui/Size = ..., kotlin/Double = ..., app.cash.redwood.ui/LayoutDirection = ...) // app.cash.redwood.ui/UiConfiguration.<init>|<init>(kotlin.Boolean;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]
Expand All @@ -110,16 +110,28 @@ final class app.cash.redwood.ui/UiConfiguration { // app.cash.redwood.ui/UiConfi
}
final object Companion { // app.cash.redwood.ui/UiConfiguration.Companion|null[0]
final fun serializer(): kotlinx.serialization/KSerializer<app.cash.redwood.ui/UiConfiguration> // app.cash.redwood.ui/UiConfiguration.Companion.serializer|serializer(){}[0]
final val $childSerializers // app.cash.redwood.ui/UiConfiguration.Companion.$childSerializers|{}$childSerializers[0]
}
final val darkMode // app.cash.redwood.ui/UiConfiguration.darkMode|{}darkMode[0]
final fun <get-darkMode>(): kotlin/Boolean // app.cash.redwood.ui/UiConfiguration.darkMode.<get-darkMode>|<get-darkMode>(){}[0]
final val density // app.cash.redwood.ui/UiConfiguration.density|{}density[0]
final fun <get-density>(): kotlin/Double // app.cash.redwood.ui/UiConfiguration.density.<get-density>|<get-density>(){}[0]
final val layoutDirection // app.cash.redwood.ui/UiConfiguration.layoutDirection|{}layoutDirection[0]
final fun <get-layoutDirection>(): app.cash.redwood.ui/LayoutDirection // app.cash.redwood.ui/UiConfiguration.layoutDirection.<get-layoutDirection>|<get-layoutDirection>(){}[0]
final val safeAreaInsets // app.cash.redwood.ui/UiConfiguration.safeAreaInsets|{}safeAreaInsets[0]
final fun <get-safeAreaInsets>(): app.cash.redwood.ui/Margin // app.cash.redwood.ui/UiConfiguration.safeAreaInsets.<get-safeAreaInsets>|<get-safeAreaInsets>(){}[0]
final val viewportSize // app.cash.redwood.ui/UiConfiguration.viewportSize|{}viewportSize[0]
final fun <get-viewportSize>(): app.cash.redwood.ui/Size // app.cash.redwood.ui/UiConfiguration.viewportSize.<get-viewportSize>|<get-viewportSize>(){}[0]
}
final enum class app.cash.redwood.ui/LayoutDirection : kotlin/Enum<app.cash.redwood.ui/LayoutDirection> { // app.cash.redwood.ui/LayoutDirection|null[0]
enum entry Auto // app.cash.redwood.ui/LayoutDirection.Auto|null[0]
enum entry Ltr // app.cash.redwood.ui/LayoutDirection.Ltr|null[0]
enum entry Rtl // app.cash.redwood.ui/LayoutDirection.Rtl|null[0]
final fun valueOf(kotlin/String): app.cash.redwood.ui/LayoutDirection // app.cash.redwood.ui/LayoutDirection.valueOf|valueOf#static(kotlin.String){}[0]
final fun values(): kotlin/Array<app.cash.redwood.ui/LayoutDirection> // app.cash.redwood.ui/LayoutDirection.values|values#static(){}[0]
final val entries // app.cash.redwood.ui/LayoutDirection.entries|#static{}entries[0]
final fun <get-entries>(): kotlin.enums/EnumEntries<app.cash.redwood.ui/LayoutDirection> // app.cash.redwood.ui/LayoutDirection.entries.<get-entries>|<get-entries>#static(){}[0]
}
final fun (app.cash.redwood.ui/Dp).app.cash.redwood.ui/toPlatformDp(): kotlin/Double // app.cash.redwood.ui/toPlatformDp|[email protected](){}[0]
final fun (app.cash.redwood.ui/Dp.Companion).app.cash.redwood.ui/fromPlatformDp(kotlin/Double): app.cash.redwood.ui/Dp // app.cash.redwood.ui/fromPlatformDp|[email protected](kotlin.Double){}[0]
final fun app.cash.redwood.ui/Margin(app.cash.redwood.ui/Dp = ...): app.cash.redwood.ui/Margin // app.cash.redwood.ui/Margin|Margin(app.cash.redwood.ui.Dp){}[0]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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.ui

public enum class LayoutDirection {
/** Horizontal layout direction from left to right. */
Ltr,

/** Horizontal layout direction from right to left. */
Rtl,

/** Horizontal layout direction is automatically inferred from included text. */
Auto,
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ public class UiConfiguration(
* raw pixels, if needed.
*/
public val density: Double = 1.0,
/**
* The device's layout direction. This defines whether `Start` alignment means left (as in
* [LayoutDirection.Ltr]) or right (as in [LayoutDirection.Rtl]), and conversely whether `End`
* alignment means right or left.
*/
public val layoutDirection: LayoutDirection = LayoutDirection.Ltr,
) {
public companion object
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import app.cash.redwood.composeui.safeAreaInsets
import app.cash.redwood.treehouse.AppService
import app.cash.redwood.treehouse.CodeListener
Expand All @@ -38,6 +40,7 @@ import app.cash.redwood.treehouse.TreehouseView.ReadyForContentChangeListener
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.OnBackPressedDispatcher
import app.cash.redwood.ui.Size
import app.cash.redwood.ui.UiConfiguration
Expand All @@ -63,6 +66,10 @@ public fun <A : AppService> TreehouseContent(
safeAreaInsets = safeAreaInsets(),
viewportSize = viewportSize,
density = density.density.toDouble(),
layoutDirection = when (LocalLayoutDirection.current) {
LayoutDirection.Ltr -> RedwoodLayoutDirection.Ltr
LayoutDirection.Rtl -> RedwoodLayoutDirection.Rtl
},
)

val treehouseView = remember(widgetSystem) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import androidx.core.view.WindowInsetsCompat
import app.cash.redwood.Modifier
import app.cash.redwood.treehouse.TreehouseView.WidgetSystem
import app.cash.redwood.ui.Density
import app.cash.redwood.ui.LayoutDirection
import app.cash.redwood.ui.Margin
import app.cash.redwood.ui.UiConfiguration
import app.cash.redwood.widget.ViewGroupChildren
Expand All @@ -36,6 +37,7 @@ import assertk.assertThat
import assertk.assertions.hasSize
import assertk.assertions.isEqualTo
import assertk.assertions.isSameInstanceAs
import java.util.Locale
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
Expand Down Expand Up @@ -137,6 +139,19 @@ class TreehouseLayoutTest {
}
}

@Test fun uiConfigurationEmitsLayoutDirectionChanges() = runTest {
val layout = TreehouseLayout(activity, throwingWidgetSystem, activity.onBackPressedDispatcher)
layout.uiConfiguration.test {
assertThat(awaitItem()).isEqualTo(UiConfiguration(layoutDirection = LayoutDirection.Ltr))

val newConfig = Configuration(activity.resources.configuration)
newConfig.setLayoutDirection(Locale("he")) // Hebrew is RTL

layout.dispatchConfigurationChanged(newConfig)
assertThat(awaitItem()).isEqualTo(UiConfiguration(layoutDirection = LayoutDirection.Rtl))
}
}

private fun viewWidget(view: View) = object : Widget<View> {
override val value: View get() = view
override var modifier: Modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package app.cash.redwood.treehouse

import app.cash.redwood.ui.Default
import app.cash.redwood.ui.Density
import app.cash.redwood.ui.LayoutDirection
import app.cash.redwood.ui.Margin
import app.cash.redwood.ui.UiConfiguration
import app.cash.redwood.widget.UIViewChildren
Expand All @@ -31,6 +32,8 @@ import kotlinx.coroutines.test.runTest
import platform.Foundation.NSDate
import platform.Foundation.NSRunLoop
import platform.Foundation.runUntilDate
import platform.UIKit.UIUserInterfaceLayoutDirection.UIUserInterfaceLayoutDirectionLeftToRight
import platform.UIKit.UIUserInterfaceLayoutDirection.UIUserInterfaceLayoutDirectionRightToLeft
import platform.UIKit.UIUserInterfaceStyle.UIUserInterfaceStyleDark
import platform.UIKit.UIView
import platform.UIKit.UIWindow
Expand Down Expand Up @@ -128,4 +131,21 @@ class TreehouseUIViewTest {
assertThat(layout.uiConfiguration.value)
.isEqualTo(UiConfiguration(safeAreaInsets = expectedInsets))
}

@Test
fun uiConfigurationReflectsInitialLayoutDirection() {
val parent = UIWindow()

val layout = TreehouseUIView(throwingWidgetSystem)
parent.addSubview(layout.view)

val expectedLayoutDirection = when (val direction = parent.effectiveUserInterfaceLayoutDirection) {
UIUserInterfaceLayoutDirectionLeftToRight -> LayoutDirection.Ltr
UIUserInterfaceLayoutDirectionRightToLeft -> LayoutDirection.Rtl
else -> throw IllegalStateException("Unknown layout direction $direction")
}
assertThat(layout.uiConfiguration.value).isEqualTo(
expected = UiConfiguration(layoutDirection = expectedLayoutDirection),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.core.graphics.Insets
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.OnBackPressedCallback as RedwoodOnBackPressedCallback
import app.cash.redwood.ui.OnBackPressedDispatcher as RedwoodOnBackPressedDispatcher
import app.cash.redwood.ui.Size
Expand Down Expand Up @@ -115,6 +116,11 @@ public open class RedwoodLayout(
safeAreaInsets = insets.toMargin(Density(resources)),
viewportSize = viewportSize,
density = density,
layoutDirection = when (config.layoutDirection) {
View.LAYOUT_DIRECTION_LTR -> LayoutDirection.Ltr
View.LAYOUT_DIRECTION_RTL -> LayoutDirection.Rtl
else -> throw IllegalArgumentException("layoutDirection must be LTR or RTL")
},
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package app.cash.redwood.widget
import app.cash.redwood.ui.Cancellable
import app.cash.redwood.ui.Default
import app.cash.redwood.ui.Density
import app.cash.redwood.ui.LayoutDirection
import app.cash.redwood.ui.Margin
import app.cash.redwood.ui.OnBackPressedCallback
import app.cash.redwood.ui.OnBackPressedDispatcher
Expand All @@ -30,6 +31,9 @@ import kotlinx.coroutines.flow.StateFlow
import platform.CoreGraphics.CGRect
import platform.UIKit.UIApplication
import platform.UIKit.UITraitCollection
import platform.UIKit.UIUserInterfaceLayoutDirection
import platform.UIKit.UIUserInterfaceLayoutDirection.UIUserInterfaceLayoutDirectionLeftToRight
import platform.UIKit.UIUserInterfaceLayoutDirection.UIUserInterfaceLayoutDirectionRightToLeft
import platform.UIKit.UIUserInterfaceStyle
import platform.UIKit.UIView

Expand All @@ -40,7 +44,13 @@ public open class RedwoodUIView(
override val children: Widget.Children<UIView> get() = _children

private val mutableUiConfiguration =
MutableStateFlow(computeUiConfiguration(view.traitCollection, view.bounds))
MutableStateFlow(
computeUiConfiguration(
traitCollection = view.traitCollection,
layoutDirection = view.effectiveUserInterfaceLayoutDirection,
bounds = view.bounds,
),
)

override val onBackPressedDispatcher: OnBackPressedDispatcher = object : OnBackPressedDispatcher {
override fun addCallback(onBackPressedCallback: OnBackPressedCallback): Cancellable {
Expand All @@ -67,13 +77,15 @@ public open class RedwoodUIView(
protected fun updateUiConfiguration() {
mutableUiConfiguration.value = computeUiConfiguration(
traitCollection = view.traitCollection,
layoutDirection = view.effectiveUserInterfaceLayoutDirection,
bounds = view.bounds,
)
}
}

internal fun computeUiConfiguration(
traitCollection: UITraitCollection,
layoutDirection: UIUserInterfaceLayoutDirection,
bounds: CValue<CGRect>,
): UiConfiguration {
return UiConfiguration(
Expand All @@ -85,6 +97,11 @@ internal fun computeUiConfiguration(
}
},
density = Density.Default.rawDensity,
layoutDirection = when (layoutDirection) {
UIUserInterfaceLayoutDirectionRightToLeft -> LayoutDirection.Rtl
UIUserInterfaceLayoutDirectionLeftToRight -> LayoutDirection.Ltr
else -> throw IllegalArgumentException("Layout direction must be RightToLeft or LeftToRight")
},
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package app.cash.redwood.widget

import app.cash.redwood.ui.Cancellable
import app.cash.redwood.ui.LayoutDirection
import app.cash.redwood.ui.OnBackPressedCallback
import app.cash.redwood.ui.OnBackPressedDispatcher
import app.cash.redwood.ui.Size
Expand Down Expand Up @@ -67,6 +68,12 @@ private class RedwoodHTMLElementView(
UiConfiguration(
darkMode = colorSchemeQuery.matches,
viewportSize = Size(width = element.offsetWidth.dp, height = element.offsetHeight.dp),
layoutDirection = when (element.dir) {
"ltr" -> LayoutDirection.Ltr
"rtl" -> LayoutDirection.Rtl
"auto" -> LayoutDirection.Auto
else -> LayoutDirection.Ltr
},
),
)

Expand All @@ -77,6 +84,7 @@ private class RedwoodHTMLElementView(
safeAreaInsets = old.safeAreaInsets,
viewportSize = old.viewportSize,
density = old.density,
layoutDirection = old.layoutDirection,
)
}
})
Expand Down Expand Up @@ -108,6 +116,7 @@ private class RedwoodHTMLElementView(
safeAreaInsets = old.safeAreaInsets,
viewportSize = old.viewportSize,
density = window.devicePixelRatio,
layoutDirection = old.layoutDirection,
)
}
}
Expand Down

0 comments on commit 41b14f6

Please sign in to comment.